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.