PageSourceSearch

https://hoist-learn.netlify.app/chunk-BnwHkzBd.js

js hoist-learn.netlify.app collected 2026-10-03 10:53:31 UTC 8,340 bytes, 26 lines download raw bytes

1import{t as e}from"./chunk-CQt8brMJ.js";var n={id:`generic-basics`,moduleId:`generics`,trackId:`typescript`,title:`Constraints, inference & defaults`,blurb:`extends as a bound, and where inference sites come from.`,minutes:12,level:`working`,tldr:{model:"Three things decide what a type parameter becomes: the inference sites that contribute candidates, the `extends` constraint that bounds it, and the `= T` default used only when nothing else determines it.",gotcha:"`extends` changes inference, not just checking — an unconstrained `T` widens literals to `string`, while `T extends string` keeps them as `'a' | 'b'`. A default never overrides a candidate inference produced."},blocks:[{kind:`prose`,md:"A generic is a **type parameter**: a hole in a signature that the caller fills. Most of the time the caller fills it without noticing, because the compiler infers it from the arguments.\n\nThree things decide what you get: the **inference sites** (which argument positions contribute a candidate), the **constraint** (`extends`, which bounds the parameter *and* changes how literals are inferred), and the **default** (`= T`, used when nothing else determines it). Almost every generics question in an interview is really about one of those three."},{kind:`heading`,text:`Inference sites`},{kind:`explainer`,id:`generics-inference`},{kind:`prose`,md:`The compiler collects a candidate for each place the type parameter appears in a parameter position, then picks a **best common supertype** from the candidates. When the candidates cannot be reconciled it does not widen quietly — it reports the mismatch against the first one it fixed on.`},{kind:`code`,lang:`ts`,caption:`Two sites, one parameter`,code:`declare function pair<T>(a: T, b: T): T;
2
3pair(1, 2);       // T = number
4pair(1, 'a');     // ?`,output:`error TS2345: Argument of type '"a"' is not assignable
5to parameter of type '1'.`},{kind:`prose`,md:"Read that error carefully — it names `1`, not `number`. The first argument fixed `T` to the literal type `1`, and the second was then checked against it. If you want a union you have to ask for one: `pair<number | string>(1, 'a')`, or give the parameters separate type parameters."},{kind:`heading`,text:`The constraint changes inference, not just checking`},{kind:`prose`,md:'This is the part people miss. `extends` is usually taught as "a bound — the argument must have at least this shape". It is that. But it also decides whether a literal type stays literal.\n\nAn **unconstrained** type parameter widens literals to their base type. A parameter constrained to `string`, `number`, or a union of literals keeps them narrow, because the constraint proves the narrow type is still legal.'},{kind:`code`,lang:`ts`,caption:`Same call, different inferred type`,code:`declare function pick1<T>(xs: T[]): T;
6declare function pick2<T extends string>(xs: T[]): T;
7
8const a1 = pick1(['a', 'b']);   // T = string
9const a2 = pick2(['a', 'b']);   // T = 'a' | 'b'
10
11const check: 'a' | 'b' = a1;`,output:`error TS2322: Type 'string' is not assignable to type '"a" | "b"'.`},{kind:`callout`,tone:`tip`,title:`The reason T extends string shows up everywhere`,md:"When you see `<K extends string>` or `<T extends PropertyKey>` in a library signature, the author usually did not need the bound for safety — they needed it so the caller's literals survive inference. It is the cheapest way to get `'a' | 'b'` instead of `string` without making everyone write `as const`."},{kind:`heading`,text:`keyof as a constraint`},{kind:`prose`,md:"The canonical constrained generic is a property getter, and it is worth being able to write from memory:\n\n`function get<T, K extends keyof T>(obj: T, key: K): T[K]`\n\nTwo parameters, one bounded by the other. `T[K]` is an **indexed access type** — the type of the property named by `K`. Because `K` is constrained to literal keys, `get(user, 'id')` returns `number` rather than `number | string`."},{kind:`code`,lang:`ts`,caption:`The signature to have in muscle memory`,code:`function get<T, K extends keyof T>(obj: T, key: K): T[K] {
12  return obj[key];
13}
14
15const user = { id: 1, name: 'Nik' };
16const id = get(user, 'id');       // number
17const nope = get(user, 'email');`,output:`error TS2345: Argument of type '"email"' is not assignable
18to parameter of type '"name" | "id"'.`},{kind:`heading`,text:`Defaults`},{kind:`prose`,md:"`<T = Something>` supplies a type only when inference produced nothing and the caller wrote nothing. It does **not** act as a fallback when inference produced a type you dislike, and it does not constrain — for that you write both: `<T extends object = {}>`.\n\nDefaults matter most on types rather than functions, where there is nothing to infer from at all: `interface Result<T, E = Error>` lets `Result<User>` mean `Result<User, Error>`."},{kind:`callout`,tone:`gotcha`,title:`A default cannot rescue a bad inference`,md:"`declare function f<T = string>
18(x: T): T` called as `f(1)` returns `number`, not `string`. Inference wins whenever it has a candidate; the default is only for the empty case. If you want to *stop* a position from contributing a candidate, that is `NoInfer`, below."},{kind:`heading`,text:`NoInfer`},{kind:`prose`,md:'Added in TypeScript 5.4. It marks a parameter position as "check against `T`, but do not vote on what `T` is".\n\nThe motivating case is a default or fallback argument that should be drawn from a set the *other* argument defines. Without `NoInfer` the fallback quietly widens the set it was supposed to belong to.'},{kind:`code`,lang:`ts`,caption:`The fallback that widens what it was meant to join`,code:`declare function pickFrom<T extends string>(vals: T[], fallback: T): T;
19
20const n1 = pickFrom(['a', 'b'], 'c');   // T = 'a' | 'b' | 'c' \u2014 'c' voted
21const n1c: 'a' | 'b' = n1;`,output:`error TS2322: Type '"a" | "b" | "c"' is not assignable to type '"a" | "b"'.
22  Type '"c"' is not assignable to type '"a" | "b"'.`},{kind:`code`,lang:`ts`,caption:`NoInfer moves the error to where it belongs`,code:`declare function pickFrom2<T extends string>(vals: T[], fallback: NoInfer<T>): T;
23
24pickFrom2(['a', 'b'], 'a');   // fine
25pickFrom2(['a', 'b'], 'c');`,output:`error TS2345: Argument of type '"c"' is not assignable
26to parameter of type '"a" | "b"'.`},{kind:`compare`,aLabel:`T extends Bound`,bLabel:`T = Default`,rows:[{label:`When it applies`,a:`Always — it bounds every use of T`,b:`Only when nothing else determines T`},{label:`Rejects bad arguments`,a:`Yes`,b:`No`},{label:`Affects literal inference`,a:`Yes — literals stay narrow`,b:`No`},{label:`Common on`,a:`Functions and types alike`,b:`Mostly types, where nothing is inferred`}]},{kind:`heading`,text:`When a generic is not a generic`},{kind:`prose`,md:"A type parameter that appears in exactly one place is a code smell — it is a union in disguise, written in a way that hides the relationship.\n\n`function log<T extends string>(m: T): void` gains nothing over `function log(m: string): void`. The point of a type parameter is to **connect two positions**: argument to return type, or one argument to another. If it appears once, delete it."},{kind:`callout`,tone:`interview`,title:`How this gets asked`,md:'*"Why does my helper return `string` when I passed `\'a\'`?"* — because the type parameter is unconstrained, so the literal widened; `extends string` fixes it. And *"when would you use a generic over a union?"* — when two positions in the signature must be the same type as each other, which a union cannot express. If you can name `NoInfer` and the problem it solves, you are visibly current with the language.'},{kind:`takeaways`,items:[`Inference collects a candidate per parameter position; conflicting candidates are an error, not a union.`,`The first argument can fix T to a literal type, which is what the confusing errors are about.`,`extends bounds the parameter and also keeps literals narrow — that is why libraries use T extends string.`,`get<T, K extends keyof T>(obj: T, key: K): T[K] is the signature to know by heart.`,`A default only fills in when inference had no candidate at all; it never overrides one.`,`NoInfer<T> lets a position be checked against T without voting on what T is.`,`A type parameter used in only one position should be a plain union instead.`]}],cards:e};export{n as GENERIC_BASICS};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.