Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 4 min read

Our tsconfig says target ES2017, and the JavaScript we ship to browsers contains ??=

Here is the whole type-checking surface of CogniPrep's tsconfig.json, which arrived with the scaffolding and has been touched by eight commits since, none of which changed the line this post is about: { "lib": ["dom

Here is the whole type-checking surface of CogniPrep's tsconfig.json, which arrived with the scaffolding and has been touched by eight commits since, none of which changed the line this post is about:

{
  "lib": ["dom", "dom.iterable", "esnext"],
  "strict": true,
  "noEmit": true,
  "module": "esnext",
  "moduleResolution": "bundler",
  "target": "ES2017"
}

target: "ES2017" reads like a promise about output: no optional chaining, no nullish coalescing, nothing past 2017 in the bundle. It is not a promise about anything. The deployment says so.

The measurement

Pick any page on the live site, collect every JavaScript chunk it references, concatenate them, and count syntax that postdates the target:

curl -s https://cogniprep.app/games/pymetrics \
  | grep -o '/_next/static/chunks/[^"]*\.js' | sort -u \
  | while read u; do curl -s "https://cogniprep.app$u"; done > chunks.js

wc -c chunks.js            # 1069163
grep -oF '?.'  chunks.js | wc -l   # 89
grep -oF '??'  chunks.js | wc -l   # 61
grep -oF '??=' chunks.js | wc -l   # 2

Eighteen chunks, 1,069,163 bytes, 89 optional chains and 61 nullish coalescing operators, two of which are the nullish assignment form. Optional chaining and ?? are ES2020. ??= is ES2021. A compiler honouring target: "ES2017" would have rewritten every one of them.

Why the line is inert

Two independent reasons, and either one alone would be enough.

Nothing in this project ever asks tsc to emit. noEmit: true is in the config, and pnpm typecheck is literally tsc --noEmit. The only compiler that produces shipped JavaScript here is the one inside next build, which is SWC. target is an instruction to the TypeScript emitter, and the TypeScript emitter never runs.

The browser syntax level is decided somewhere else entirely. Next picks its own browser targets, and the knob that overrides them is a browserslist key in package.json or a .browserslistrc file. This repository has neither, so Next's defaults apply, and those defaults consider ?. safe to ship. If you want the output to actually be ES2017, browserslist is the only line that gets you there; editing target gets you nothing but a different opinion in a file nobody consults at build time.

There is a third reason the line does even less than it looks: lib is set explicitly to ["dom", "dom.iterable", "esnext"]. Normally target implies a default lib, so ES2017 would mean the typechecker refuses Object.fromEntries or String.prototype.replaceAll. The explicit esnext entry overrides that, so every modern standard library method type checks fine. Target is not even restricting what we are allowed to write.

The one thing it still decides

useDefineForClassFields defaults to true when target is ES2022 or above and false below it. We are below it and we have not set the flag, so it is false, which means TypeScript type checks class fields with the old assignment semantics rather than Object.defineProperty semantics.

Twelve class properties in this codebase are initialised that way, including two in the repository that handles cached population statistics:

getCachedStatsWithDefault = (gameId: string) => { ... };
getCachedStatsWithDefaultRSC = cache((gameId: string) => this.getCachedStatsWithDefault(gameId));

The two semantics differ in real, observable ways: the order fields initialise relative to the constructor, whether a field shadows an accessor from a base class, and whether declare is required for a re-declared field. So the honest summary is that one inert line still picks the class-field semantics the typechecker assumes, while the bundler picks the semantics that actually run.

I have not found a bug from that gap, and I am not going to claim there is one. The point is narrower and worse: the config line that looks like it controls output controls only an edge case nobody chose, and the thing that does control output is absent from the repository.

What I take from it

  • Verify compiler settings against build output, not against the config file. A string in a config is a hypothesis. One curl and one grep on a deployed chunk settles it, and that check is cheap enough to run the first time you inherit a project.
  • Template defaults are not decisions. target: ES2017 arrived with the scaffolding, outlived two framework major versions, and was never load-bearing. Deleting it would change nothing I can measure; the only reason to keep it is that lowering it or raising it would silently move useDefineForClassFields.
  • If the syntax level matters to you, say so in browserslist. That is the file the build reads. Everything else is a comment.

See it

The chunks in that pipeline are the real thing, so the check works on any route: the provider catalogue, a single provider's page, or the pricing page if you want a smaller page to grep. Run it on your own deployment too. If your tsconfig.json says ES2017 and your bundle says otherwise, the tooling is not lying to you: you were reading the wrong file.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.