<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ryuya]]></title><description><![CDATA[Ryuya]]></description><link>https://ryuyah.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 24 Oct 2026 01:48:39 GMT</lastBuildDate><atom:link href="https://ryuyah.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your tsconfig target is not a browser support policy]]></title><description><![CDATA[How a noLib config with typescript-baseline-lib and modern-web-types type-checks JavaScript built-ins against a Baseline target, on TypeScript 6 and 7]]></description><link>https://ryuyah.hashnode.dev/tsconfig-target-is-not-a-browser-support-policy</link><guid isPermaLink="true">https://ryuyah.hashnode.dev/tsconfig-target-is-not-a-browser-support-policy</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Ryuya]]></dc:creator><pubDate>Fri, 16 Oct 2026 16:47:19 GMT</pubDate><content:encoded><![CDATA[<p>In January 2024, someone following the TanStack Router quick start opened <a href="https://github.com/TanStack/router/issues/925">issue #925</a>: the app crashed in Safari 15.1 with <code>TypeError: (this.state.pendingMatches || this.state.matches).at is not a function</code>, and the same code worked in Safari 16.1. <code>Array.prototype.at</code> arrived in Safari 15.4. A commenter on the issue reported that adding Babel to transpile the code did not help. That is expected when no polyfill is loaded: transpiling rewrites syntax, and <code>.at()</code> is a method that has to exist in the browser at runtime. The router fixed it by replacing the <code>at</code> call in <a href="https://github.com/TanStack/router/pull/986">PR #986</a>.</p>
<p>A common answer to "which browsers does this project support?" is a line in <code>tsconfig.json</code>, something like <code>"target": "es2022"</code>. I understand why. It looks like a compatibility setting. But it isn't one, and this post is about what to use instead if you want the type checker to know about your browser target.</p>
<p>I'll stay in the type layer. Linting gets a short mention at the end.</p>
<h2>What target and lib actually promise</h2>
<p><code>target</code> decides which syntax TypeScript emits and, when you don't set <code>lib</code>, which declaration files get loaded by default. <code>lib</code> decides which built-in APIs the checker believes exist. Neither one adds code at runtime. If <code>lib</code> declares <code>Set.prototype.difference</code>, TypeScript accepts the call, and whether it runs depends entirely on the browser.</p>
<p>ECMAScript years also don't line up with browsers. A feature lands in a spec year, then ships in each engine on its own schedule, then takes time to reach the devices your users actually have. The author of <a href="https://github.com/microsoft/TypeScript/issues/62536">TypeScript issue #62536</a> describes using <code>lib: "ES2022"</code> as a stand-in and notes that it is "not self-updating and could still include items that some browsers are yet to support". That issue asks for a <code>"baseline-widely-available"</code> lib value. It is open, labeled Suggestion and Awaiting More Feedback.</p>
<p>Until something like that exists in TypeScript, the lib has to come from a package.</p>
<h2>Replacing the JavaScript lib</h2>
<p><a href="https://github.com/3ru/TypeScript-Baseline-lib-generator"><code>typescript-baseline-lib</code></a> is a generated set of declarations for JavaScript built-ins, filtered by <code>web-features</code> data to a Baseline target. Version 0.1.1 uses a snapshot from 2026-10-02 and targets Widely available. A built-in outside the target simply has no declaration.</p>
<p>Because it replaces the standard libs instead of adding to them, it runs under <code>noLib</code>. Here is the config I use next to an ordinary <code>tsconfig.json</code>:</p>
<pre><code class="language-json">// tsconfig.baseline.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "lib": null,
    "noLib": true,
    "libReplacement": false,
    "noEmit": true,
    "skipLibCheck": false,
    "types": ["typescript-baseline-lib", "@typescript/lib-dom"]
  },
  "include": ["src/**/*.ts"]
}
</code></pre>
<p>Line by line:</p>
<ul>
<li><code>"lib": null</code> drops the <code>lib</code> array inherited from the base config, which in my case is <code>["ESNext", "DOM"]</code>.</li>
<li><code>"noLib": true</code> tells TypeScript not to load any of its bundled <code>lib.*.d.ts</code> files.</li>
<li><code>"libReplacement": false</code> turns off lib-name lookup, which has nothing to resolve once <code>noLib</code> is on.</li>
<li><code>types</code> brings in the Baseline JavaScript declarations and a DOM lib. With <code>noLib</code>, DOM is not loaded for you, so it has to be listed. I use <code>modern-web-types</code> installed under the <code>@typescript/lib-dom</code> alias.</li>
<li><code>"skipLibCheck": false</code> makes TypeScript check the declaration files against each other, which is how I confirmed the two packages fit together.</li>
</ul>
<p>Sample input:</p>
<pre><code class="language-ts">// src/builtins.ts
const current = new Set(["a", "b", "c"]);
const removed = new Set(["b"]);
export const kept = current.difference(removed); // set-methods: Newly available 2024-06-11

export const answer = Promise.try(() =&gt; 42); // promise-try: Newly available 2025-01-07
</code></pre>
<p>The base config passes this file because <code>ESNext</code> declares both methods. The Baseline config does not:</p>
<pre><code class="language-text">$ npx tsc -p tsconfig.baseline.json
src/builtins.ts(3,29): error TS2550: Property 'difference' does not exist on type 'Set&lt;string&gt;'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2025' or later.
src/builtins.ts(5,31): error TS2550: Property 'try' does not exist on type 'PromiseConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2025' or later.
</code></pre>
<p>The "try changing the lib" hint is TypeScript's standard TS2550 text. In this setup, following it would remove the check.</p>
<h2>Why it has to be noLib</h2>
<p>The tempting shortcut is to leave <code>lib</code> alone and add the package to <code>types</code>:</p>
<pre><code class="language-json">{
  "compilerOptions": {
    "lib": ["DOM"],
    "libReplacement": true,
    "types": ["typescript-baseline-lib"]
  }
}
</code></pre>
<p>In my test project on TypeScript 6.0.3, that produced 42 errors inside declaration files, in addition to the expected TS2550 errors in source:</p>
<pre><code class="language-text">node_modules/typescript-baseline-lib/baseline.d.ts(765,6): error TS2300: Duplicate identifier 'IteratorResult'.
node_modules/typescript/lib/lib.es2015.iterable.d.ts(37,6): error TS2300: Duplicate identifier 'IteratorResult'.
node_modules/typescript/lib/lib.es5.d.ts(118,5): error TS2374: Duplicate index signature for type 'number'.
</code></pre>
<p>The config only asks for <code>DOM</code>, so where do <code>lib.es5.d.ts</code> and <code>lib.es2015.iterable.d.ts</code> come from? <code>--explainFiles</code> answers it:</p>
<pre><code class="language-text">node_modules/typescript/lib/lib.es5.d.ts
  Library referenced via 'es5' from file 'node_modules/typescript/lib/lib.es2015.d.ts'
node_modules/typescript/lib/lib.es2015.d.ts
  Library referenced via 'es2015' from file 'node_modules/@typescript/lib-dom/lib.dom.d.ts'
</code></pre>
<p>The DOM file starts with <code>/// &lt;reference lib="es2015" /&gt;</code>, the same as <code>@types/web</code>. Outside <code>noLib</code>, TypeScript follows that reference and loads the ES2015 libs, which pull in ES5. Now the standard declarations and the Baseline declarations both define <code>IteratorResult</code>, <code>PropertyKey</code>, <code>Partial</code>, and the rest, and the checker reports every collision.</p>
<p>Under <code>noLib</code>, those references were not followed. <code>--listFilesOnly</code> on the working config shows exactly five files: my two sources, <code>typescript-baseline-lib/baseline.d.ts</code>, <code>typescript-baseline-lib/index.d.ts</code>, and <code>@typescript/lib-dom/lib.dom.d.ts</code>. With <code>skipLibCheck: false</code> there were no declaration errors.</p>
<p>So the rule is: one config with <code>noLib</code>, both packages in <code>types</code>. A single <code>noLib</code> tsconfig also worked for me. I keep it separate so the editor uses the wider <code>ESNext</code> lib and CI runs the Baseline one.</p>
<h2>TypeScript 6 and 7</h2>
<p>I ran the same configs on TypeScript 6.0.3 and 7.0.2. The diagnostics matched: the two TS2550 errors from the Baseline config, a clean run from the base config, and duplicate-declaration errors (TS2300, TS2374) from the non-<code>noLib</code> shortcut. The only difference I saw was the exit code on failure, 2 on 6.0.3 and 1 on 7.0.2. Both are non-zero, so CI treats them the same.</p>
<p>I only compared <code>tsc</code> output on 7.0.2. Editor behavior and other tooling are a separate question. I stayed on 6.0.3 for the project because <code>typescript-eslint</code> 8.71.1 declares a TypeScript peer range of <code>&lt;6.1.0</code>.</p>
<h2>DOM is a separate layer</h2>
<p>This config is responsible for JavaScript built-ins. DOM types come from whatever you list next to it. With <code>modern-web-types</code>, you get every API shipped in at least one stable engine, which is what you want for writing progressive enhancements. If you would rather have the type system enforce DOM support too, uhyo's <a href="https://github.com/uhyo/TypeScript-Baseline-Types"><code>@baseline-types/dom-*</code></a> packages provide DOM libs cut to a Baseline target. Those are two different answers to the DOM question, so pick one per config.</p>
<h2>Declaring a polyfill</h2>
<p>If you ship a polyfill, add its feature explicitly:</p>
<pre><code class="language-json">"types": [
  "typescript-baseline-lib",
  "typescript-baseline-lib/allow/promise-try",
  "@typescript/lib-dom"
]
</code></pre>
<p>With that entry, <code>Promise.try</code> type-checked and <code>Set#difference</code> was still an error. An <code>allow/*</code> entry only adds declarations, so you still need the import that loads the polyfill (I used <code>core-js/proposals/promise-try</code>).</p>
<p>For feature-detected code, the pattern I use is an intersection type with the member marked optional, so the compiler requires a <code>typeof</code> check before the call:</p>
<pre><code class="language-ts">type MaybeSetMethods&lt;T&gt; = Set&lt;T&gt; &amp; {
  difference?: (other: ReadonlySet&lt;T&gt;) =&gt; Set&lt;T&gt;;
};
</code></pre>
<h2>Do you need this?</h2>
<p>If your project never touches built-ins newer than your <code>lib</code> year, and you are happy reviewing that by hand, probably not. If you already lint against a Browserslist config using <a href="https://github.com/amilajack/eslint-plugin-compat"><code>eslint-plugin-compat</code></a> or <a href="https://github.com/koddsson/eslint-plugin-tscompat"><code>@koddsson/eslint-plugin-tscompat</code></a>, that covers similar ground at the lint layer. Browserslist also accepts Baseline queries such as <code>baseline widely available</code> if you want one policy name across tools.</p>
<p>What the type layer adds is that built-ins outside the target are absent from the declarations. The error shows up in <code>tsc</code>, at the line, before lint runs.</p>
<p>For DOM APIs and per-call checks I use <a href="https://github.com/3ru/eslint-plugin-baseline-js">eslint-plugin-baseline-js</a> with its type-aware preset. I walk through that layer, with the ESLint config and CI scripts, in <a href="https://dev.to/ryuyah/type-checks-pass-browsers-crash-adding-a-baseline-check-to-a-typescript-project-4gl5">a separate post on DEV</a>. The choice of target belongs to your audience data, and web.dev's <a href="https://web.dev/articles/how-to-choose-your-baseline-target">How to choose your Baseline target</a> covers that. Whichever target you pick, pin the package version: Widely available moves with each data release, and a bump should be a change you review.</p>
<p>Disclosure: I maintain eslint-plugin-baseline-js and typescript-baseline-lib.</p>
]]></content:encoded></item></channel></rss>