Vibe coding head to head


For a long time I had an idea on my BLOG todo list: writing an article about using TypeScript together with either Duktape or QuickJS.

Duktape or QuickJS are embedable JavaScript implementations. So my idea is to use this to host TypeScript's compiler and avoid having to install Node.js at all.

Duktape was quickly discarded as it implements an older, incomplete version of JavaScript that doesn't match the modern ES2015+ features required by the TypeScript compiler.

Recently I got a license to for Claude Pro as well as a DeepSeek API. And I figure this could make an excellent use-case for comparing Claude vs DeepSeek.

For this comparison I am using Claude Code. Claude Code is Anthropic's AI-powered coding agent. It can search codebases, trace dependencies, create and edit files across a project, build new features, and execute multi-file refactors at scale — work that can save days of engineering effort.

Using Claude Code, I tested:

  • Sonnet 4.6
  • deepseek-v4-flash

I ran these through three iterations of essentially the same prompts.

v1

The first prompt just gave instructions on using QuickJS to run TypeScript compiler without using Node.js.

Both AI's took the approach of downloading QuickJS and TypeScript and embed them into a single binary executable.

  • DeepSeek used TypeScript's ts.transpileModule() API — transpile only, no type checking. The C code (449 lines of main.c) embedded the TypeScript JS bundle via ld -r -b binary, an ELF-specific trick that wraps the blob into an object file accessed through linker symbols. The Node.js shim was a ~50-line C string literal.

  • Claude embedded the full tsc.js CLI bundle — the same thing that runs when you type npx tsc. It used Python scripts (gen_embed.py) to convert JS files into C byte arrays at build time, which is portable across Linux and macOS. The shim was a proper 226-line shims.js file, evaluated separately.

The verdict from v1 was decisive: Claude had the better implementation for real use. Running actual tsc with full type checking and tsconfig.json support is categorically more useful than transpile-only. DeepSeek's only genuine advantage is zero build-time dependencies — no Python, no curl, no tar. Just gcc and make.

But both had issues. DeepSeek broke source maps (the sourceMapText from transpileModule() is silently discarded), had a fixed 8KB options buffer that truncates silently, and used technically undefined behavior to access the blob size. Claude had a duplicate process.stdout declaration, used strlen(data) instead of the JS string length in js_write_file, and hardcoded process.platform to 'linux' even when built on macOS.

v2

Afterwards, I did some interactive prompting to improve the code in both solutions. But I have to admit that I did more meddling with the DeepSeek codebase as compared to Claude's.

DeepSeek ballooned to 807 lines of C with a ~240-line bootstrap JS string inlined as a C literal. It introduced a custom compiler host that calls ts.createProgram() for type checking — a genuinely hard thing to do correctly. The C↔JS communication went through JSON serialization.

But this is where things got ugly. The post-mortem identified six concrete bugs:

  1. use_stdin is never set — declared and initialized to 0, nothing ever sets it to 1. The documented stdin transpile mode is completely dead code.
  2. Fixed-size stack buffers (paths_buf[16384], opts_buf[8192], lib_json[4096]) truncate silently, producing malformed JSON.
  3. JSON injection risk — paths are inserted into JSON strings with a comment saying "no quotes/backslashes expected." Not enforced.
  4. fexists() reads entire files — every existence check calls tryLoad, which reads the whole file. Large .d.ts files get read twice.
  5. Fragile JSON parsingstrncmp(res_str, "{\"success\":true", 15) to check success. Breaks on whitespace changes.
  6. directoryExists too narrow — subdirectories always return false, confusing TS module resolution.

Claude kept its clean three-layer design (C bindings → Node.js shims → tsc.js) and grew a proper test suite with 10 tests. Its issues were cosmetic: an orphaned gen_libs.py that generates a C lookup table for lib files but is never wired into the Makefile, the same duplicate process.stdout, and the hardcoded platform string.

The v2 verdict: Claude remained the better-engineered project. DeepSeek's v2 had bugs that would cause real, observable failures. Claude v2's issues were cosmetic or edge-case.

v3

For v3, I started again, and installed on my laptop a QuickJS version compiled by my distro and instructed the AI to simply use that to run TypeScript tsc.js.

DeepSeek went "bundle-first" — it pre-concatenated the shim and tsc.js into a single ~6 MB tsc-bundle.js. One command to run. Easy for end users. But the shim quality regressed in critical ways:

  • crypto.createHash always returns the same fixed string ("abcdef1234567890abcdef1234567890" for every input). This silently corrupts .tsbuildinfo — content hashes always match, so tsc's incremental cache sees files as never changed when they have. If you use --build or incremental mode, you get wrong output and you don't know it.

  • Buffer.from(data, encoding) ignores the encoding argument. Treats all strings as ASCII. Multi-byte UTF-8 content gets silently corrupted.

  • process.env reads /proc/self/environ at load time — Linux-only, breaks silently on macOS even though QuickJS runs there. Also snapshots the environment once, so runtime changes are invisible.

  • path.join('/a', '/b') yields /a//b instead of /b — doesn't reset at absolute mid-arguments like Node.js does.

Claude took a different approach:

  • require('crypto') intentionally throws — this triggers tsc's fallback to its built-in generateDjb2Hash, which is a real hash function. Incremental builds work correctly because the author understood that sometimes the right implementation is "don't implement it, let the library handle itself."

  • Buffer is a proper class _Buffer extends Uint8Array with full support for utf8, hex, base64, utf16le, ascii, and latin1 encodings. Integer read methods (readUInt8, readInt8, readUInt16LE, etc.) included.

  • process.env is a Proxy backed by std.getenv() — lazy, on-demand, cross-platform, and always current.

  • 111 tests vs DeepSeek's 54, with unit-level coverage of Buffer, path, fs, os, process, require, and TextEncoder/TextDecoder — not just end-to-end integration smoke tests.

Claude's only real bug is a latin1 encoding mask that uses 0x7F instead of 0xFF, which corrupts high-byte characters. (Though in practice, tsc probably never triggers it.)

The Pattern

Across all three versions, one pattern holds: Claude consistently makes the harder, more durable choice.

Decision DeepSeek's approach Claude's approach
TS invocation Custom host calling API methods Runs real tsc.js unmodified
Shim embedding Inline C string literal Separate JS file, embedded at build time
Build deps Zero (vendored everything) Python, curl, tar (fetch at build)
Lib files On disk at runtime Embedded or found via search path
Crypto Stubbed with fake hash Intentionally throws → tsc fallback
Buffer Minimal, encoding-ignorant Full class, all major encodings
Testing End-to-end smoke tests Unit + integration (2x coverage)
Portability Linux only (ELF blob) Linux + macOS (C byte arrays)

DeepSeek's projects are consistently easier to get started with — fewer build deps, simpler code, vendored everything. But they accumulate bugs faster because they're fighting against the complexity of the TypeScript API surface. Every new TypeScript feature that touches the runtime needs a shim update.

Claude's approach — let tsc.js run itself, provide a convincing Node.js facade, get out of the way — is more work upfront (figuring out exactly which Node.js APIs tsc touches is non-trivial) but pays off in maintenance. Upgrading TypeScript is just dropping in a new tsc.js. The shim doesn't need to know anything about TypeScript internals.

The Real Lesson

The most important takeaway isn't about TypeScript or QuickJS at all. It's about the shape of the problem.

DeepSeek tries to control TypeScript: wrap it in a custom host, serialize options as JSON, intercept its output. This works until TypeScript does something the host didn't anticipate.

Claude tries to become Node.js: provide the environment tsc expects, then get out of the way. This is more work initially but scales with the upstream.

DeepSeek's projects are fascinating engineering artifacts — compact, clever, audacious. Claude's projects are useful tools. There's a lesson there about knowing whether you're building a proof of concept or a product.

Cost comparisons

Here is a comparison of the costs for using DeepSeek's API platform versus a Claude Pro subscription.

The fundamental difference is that DeepSeek's API charges per token (pay-as-you-go) , while Claude Pro is a fixed monthly subscription for a chat interface with usage limits. Your choice depends entirely on your usage volume: DeepSeek is drastically cheaper per token, while Claude Pro offers predictability and access to more capable coding models within a cap.

Feature DeepSeek API (V4) Claude Pro / Max Subscription
Pricing Model Pay-per-token (consumption-based) Fixed monthly fee
Lowest Cost $0.0000035 per 1M input tokens (with cache + promotion) $20 per month (flat rate)
Typical Cost Input: $1 - $3 per 1M tokens
Output: $2 - $6 per 1M tokens (for Pro model)
$20/month (Pro) gives ~45 messages per 5 hours
$100-200/month (Max) for higher limits
Best For Low-volume testing, high-volume automated tasks, or when cost control is critical Unlimited chat access within rate limits, predictable billing, and access to Claude's strongest models
Real-World Cost ~$30-75/month for 1M input/1M output on DeepSeek V4
~$450-900/month for same on Claude Sonnet 4.6
Light use: $20/month (stay within limits)
Heavy API use: Developers report $500-2,000/month on Claude API

Key Insights for Comparison

  1. Scale and Predictability: If you are a developer who uses AI coding assistance casually (e.g., a few dozen queries a day), the Claude Pro plan's fixed $20/month cost is predictable and simple. However, if you exceed the message limits, you'll be rate-limited. For heavy users, the Claude Max plan costs $100-200/month.
  2. Incredible DeepSeek Value: For any usage that requires processing millions of tokens (e.g., working with large codebases, document analysis, RAG applications), DeepSeek's API is dramatically cheaper. At the promotional cached rate of $0.025 per 1M tokens, you could process 800 million tokens for the same $20 as a single month of Claude Pro.
  3. Model Capability Trade-off: The price difference reflects a capability difference. For complex "agentic coding" tasks, Claude's Opus and Sonnet models (available via API or Pro subscriptions) are still considered the industry leaders, with DeepSeek noting its V4 code quality is close to but still behind Claude Opus 4.6. For most other tasks, DeepSeek V4 provides exceptional value at a fraction of the cost.
  4. API vs. Subscription Access: The most cost-effective approach for a heavy user might be to use the DeepSeek API via a pay-as-you-go service for most tasks and keep a Claude Pro subscription for complex problems requiring Claude's strongest models.

Final thoughts

So in general, in my opinion Claude is generally better at coding. DeepSeek is good enough, but one requires proper attention and supervision on what is doing. Where it makes a big difference is in pricing. DeepSeek is indeed a fraction of what Claude would cost otherwise.

Artifacts

You can find the generated code in my github repository here.

The final TypeScript based on Claude was tweaked and cleaned up and bundled into a repo on github here.