cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Generic constraints

Limit a TypeScript type parameter with extends so you can use its length or properties safely, allow only real keys with keyof, and avoid over-constraining.

By the end, you can
  • 01
    Add a constraintWrite T extends { length: number } so the function may use .length, and read the error when an argument doesn't fit.
  • 02
    Allow only real keysUse K extends keyof T so a function accepts only property names the object has, and gives back that property's type.
  • 03
    Ask for just enoughSpot a constraint that turns away good values or loses a type, and replace it with one that asks only for what the function uses.

Telling TypeScript what T must have

When a shopper signs in, a shop page may find two carts: the one saved on this device and the one saved in their account. It keeps the longer one. A generic longest should work for any two lists. Here is a first try, with a plain type parameter T (the Generic functions lesson covers type parameters):

A T that might not have a length (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function longest<T>(a: T, b: T): T {  return a.length >= b.length ? a : b;} console.log(longest(["Tea"], ["Tea", "Cake"]));

Line 1 says a and b are both T, and longest gives back a T. Line 2 compares their lengths and gives back the longer one. TypeScript marks both .length reads on line 2, on purpose:

  • Property 'length' does not exist on type 'T' means "T could be any type, and not every type has a length". A caller could pass two numbers, and numbers have no length.

Line 5 passes two carts, so the code prints [ 'Tea', 'Cake' ] when it runs. But TypeScript checks the function for every possible T, not just this call. You need a way to say "T may be any type, as long as it has a length". That is a constraint:

T must have a lengthPop out in the code editor (opens in a new tab)TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {  return a.length >= b.length ? a : b;} const savedCart = ["Tea"];const newCart = ["Tea", "Cake"];const cart = longest(savedCart, newCart);//    ^?console.log(cart);

The only change is on line 1: T extends { length: number }. It says T must be a type with a length that is a number. Now every T has a length, so line 2 is allowed. Lines 5 and 6 make the two carts, and line 7 keeps the longer. The query on line 8 shows const cart: string[]: T is still exactly the type that went in, not just "something with a length". Line 9 prints [ 'Tea', 'Cake' ].

Left, no constraint: a box labelled T holds a card with a question mark. It could be any type, and a.length is an error. Right, T extends { length: number }: the box labelled T holds a card that says length. It has at least a length, and a.length is fine.<T>?Tcould be any typea.length: error<T extends { length: number }>lengthThas at least a lengtha.length: fine
With a plain <T>, TypeScript knows nothing about what's inside, so a.length is an error. With T extends { length: number }, every T has a length, so line 2 may read it.
Real-life analogyA cabin bag frame at the airport

Before you board a plane, your cabin bag must fit a metal frame at the gate. Any bag that fits is allowed: a backpack, a small suitcase, a tote. The frame doesn't swap your bag for a standard one. You carry your own bag on board.

In real life: The frame at the gate says what size a bag must be
In TypeScript: The constraint { length: number } says what T must have
In real life: A backpack or a small suitcase: any bag that fits
In TypeScript: A string, an array, or any object with a length
In real life: A bag too big for the frame is turned away
In TypeScript: longest(10, 20) gets an error: numbers have no length
In real life: You still carry your own bag on board
In TypeScript: longest gives back a string[], not just "something with a length"
The main idea

A generic constraint, written T extends SomeType, limits which types T may be. Every T must have at least what SomeType has, so inside the function you may use it. Each call still fills in T with the exact type of its arguments.

Constraints with extends

In angle brackets, extends means "must fit". T extends { length: number } lets T be any type that has at least a length that is a number. Extra properties are fine. TypeScript compares shapes, not names (the Structural typing lesson explains this). Press the buttons to pass different arguments on line 5:

Playground: what fits the constraint?
The longer of two valuesPop out in the code editor (opens in a new tab)TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {  return a.length >= b.length ? a : b;} const result = longest(["Tea"], ["Tea", "Cake"]);//    ^?console.log(result);
result's typestring[]
What happens
TypeScriptNo errors
Console output[ 'Tea', 'Cake' ]
Try it yourself

Arrays have a length, so both carts fit the constraint. T is string[], and result keeps that type: you could call result.push("Pie"). It prints the longer cart.

TypeScript checks the code for real, and the output is what the code prints. Each button changes only line 5. Reset goes back to Two carts.

Two carts fit, and result is string[]. Two numbers get Argument of type 'number' is not assignable to parameter of type '{ length: number; }': in plain words, "a number doesn't have the length this function needs". Two objects with a length fit, though they're plain objects. A cart and a number shows that every argument typed T is checked.

Three values meet the constraint { length: number }. The cart ["Tea", "Cake"] has a length, so it fits and T is string[]. The object { length: 5 } fits too. The number 10 has no length, so it gets an error.["Tea", "Cake"]{ length: 5 }10{ length: number }fits: T is string[]fitsno length: error
The constraint checks each argument. The cart and the object with a length fit. 10 has no length, which is the error on line 5 when you press Two numbers.

A constraint can be any type, including one you named with type. Strings have a length too:

A named constraintPop out in the code editor (opens in a new tab)TypeScript
type HasLength = { length: number }; function longest<T extends HasLength>(a: T, b: T): T {  return a.length >= b.length ? a : b;} const tags = longest(["sale"], ["sale", "new"]);//    ^?console.log(tags.join(", "), longest("Tea", "Cake"));

Line 1 gives the shape a name, HasLength, and line 3 uses it: T extends HasLength. Line 7 passes two lists of tags, and line 8 shows const tags: string[]. Line 9 prints sale, new, then calls longest("Tea", "Cake"), which prints Cake: four letters beat three.

Why not skip T and type the parameters as { length: number }? Because then nothing connects what goes in with what comes out:

Plain parameter types lose the cart (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function longest(a: { length: number }, b: { length: number }) {  return a.length >= b.length ? a : b;} const cart = longest(["Tea"], ["Tea", "Cake"]);//    ^?cart.push("Pie");

Line 1 types both parameters as "something with a length". Line 5 passes two carts, but the query on line 6 shows { length: number; }: that's all TypeScript knows about the result. Line 7 tries to add to the cart and gets Property 'push' does not exist on type '{ length: number; }'. The type parameter in the overview kept string[].

A constraint is the least T has, not all it has. So a function that promises to give back a T can't give back a new object that only matches the constraint:

Returning the constraint instead of T (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function atLeast<T extends { length: number }>(value: T, minimum: number): T {  if (value.length >= minimum) {    return value;  }  return { length: minimum };}

Line 1 promises to give back a T. Line 3 gives back value, which is a T, so that's fine. Line 5 gives back a new object { length: minimum }. TypeScript marks it with a message in two lines:

  • Type '{ length: number; }' is not assignable to type 'T': this object isn't necessarily a T.
  • 'T' could be instantiated with a different subtype of constraint '{ length: number; }': "a caller could fill in T with a more specific type". If they pass a cart, T is string[], and they expect a cart back, with push and map, not a plain object.

Only real property names with keyof

A settings page reads one setting at a time by its name. Names are text, but not any text: only the names the settings object has. TypeScript has a word for that list. keyof before an object type gives the names of its properties:

The names of a type's properties (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: string; fontSize: number; showTips: boolean }; type SettingName = keyof Settings;const picked: SettingName = "fontSize";const typo: SettingName = "colour";

Line 1 describes the settings. Line 3 makes SettingName the type keyof Settings, which means "theme" | "fontSize" | "showTips": one of those three names, as text. Line 4 picks "fontSize", which is fine. Line 5 tries "colour" and gets an error on purpose: Type '"colour"' is not assignable to type 'keyof Settings', "Settings has no property called colour". (The keyof & typeof lesson covers keyof in depth.)

You can type a parameter with keyof directly. That stops typos, but the result is vague:

A key typed with keyof, and a vague resultPop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: string; fontSize: number; showTips: boolean }; const settings: Settings = { theme: "dark", fontSize: 16, showTips: true }; function getSetting(key: keyof Settings) {  return settings[key];} const size = getSetting("fontSize");//    ^?console.log(size);

Line 5 says key is one of the setting names. Line 9 asks for "fontSize", but the query on line 10 shows const size: string | number | boolean. The function doesn't know which name you passed, so the result could be any setting's type. Line 11 prints 16.

Make the key a type parameter instead: K extends keyof Settings. Now K is the exact name you pass, and the return type Settings[K] means "the type of property K in Settings". Type a name on line 9, and watch the error list and the answer after ^?:

Playground: type a setting's name
Read one setting by namePop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: string; fontSize: number; showTips: boolean }; const settings: Settings = { theme: "dark", fontSize: 16, showTips: true }; function getSetting<K extends keyof Settings>(key: K): Settings[K] {  return settings[key];} const value = getSetting("fontSize");//    ^?console.log(value);
The key"fontSize"
What happens
value's typenumber
TypeScriptNo errors
When it runsprints 16
Try it yourself

"fontSize" is one of the names in keyof Settings, so K is "fontSize". Settings[K] is the type of that property, number, and the code prints 16.

The code is checked by the real TypeScript compiler each time you type. Only line 9 changes. Reset puts "fontSize" back.

With "fontSize", value is a number. With "theme", it's a string, and with "showTips", a boolean. "colour" gets Argument of type '"colour"' is not assignable to parameter of type 'keyof Settings'. theme without quotes gets Cannot find name 'theme', because a name without quotes means a variable.

A Settings window lists theme: string, fontSize: number, and showTips: boolean. An arrow marked keyof leads to three names: "theme", "fontSize", and "showTips". The name "fontSize" leads to its type, number.Settingstheme: stringfontSize: numbershowTips: booleankeyof"theme""fontSize""showTips"the namesnumberits type
keyof Settings is the list of property names, "theme" | "fontSize" | "showTips". getSetting("fontSize") picks one, and Settings[K] gives back that property's type, number.

Constraints that ask for too much

A constraint should ask for what the function uses, and no more. Here an order search asks for a name it never reads:

A constraint that turns away orders (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Order = { id: number; total: number }; function findById<T extends { id: number; name: string }>(items: T[], id: number) {  return items.find((item) => item.id === id);} const orders: Order[] = [{ id: 7, total: 9 }];const order = findById(orders, 7);

Line 1 says an Order has an id and a total. Line 3 asks every item for an id and a name, but line 4 only reads id. Line 8 passes the orders, and TypeScript turns them away: Argument of type 'Order[]' is not assignable to parameter of type '{ id: number; name: string; }[]', followed by the reason, Property 'name' is missing in type 'Order'. Ask only for the id:

A constraint that asks for what it usesPop out in the code editor (opens in a new tab)TypeScript
type Order = { id: number; total: number }; function findById<T extends { id: number }>(items: T[], id: number) {  return items.find((item) => item.id === id);} const orders: Order[] = [{ id: 7, total: 9 }];const order = findById(orders, 7);//    ^?console.log(order?.total);

Line 3 now asks only for id. Line 8 is fine, and line 9 shows const order: Order | undefined: T is still Order, so order?.total on line 10 is allowed and prints 9.

Left: the constraint { id: number; name: string } meets the order { id: 7, total: 9 }. The order has no name, so it is turned away. Right: the constraint { id: number } meets the same order, and it fits, so T is Order.{ id: number; name: string }{ id: 7, total: 9 }turned away: no namethe function never reads name{ id: number }{ id: 7, total: 9 }fits: T is Orderit asks for what it uses
The same order, two constraints. Asking for a name that findById never reads turns the order away on line 8. Asking only for an id lets it in, and T stays Order.

Another way to ask for too much is to make the type parameter the whole list, when you only need its items:

A constraint on the whole list loses the item typePop out in the code editor (opens in a new tab)TypeScript
function firstItem<T>(list: T[]) {  return list[0];}function firstOfList<L extends any[]>(list: L) {  return list[0];} const price = firstItem([3, 4]);//    ^?const lost = firstOfList([3, 4]);//    ^?

Lines 1 to 3 use T for one item, and list: T[]. Lines 4 to 6 use L extends any[] for the whole list. Both give back list[0]. Line 9 shows const price: number, but line 11 shows const lost: any: TypeScript reads the item through the constraint any[], so the type is gone. Let the type parameter be the simplest thing you need, here one item.

Finally, a function may not need a type parameter at all. If T would appear only once, write the constraint as a plain parameter type:

A plain parameter type is enough herePop out in the code editor (opens in a new tab)TypeScript
function isTooLong(value: { length: number }, max: number) {  return value.length > max;} console.log(isTooLong("Asha", 3));console.log(isTooLong(["Tea", "Cake"], 3));

Line 1 takes anything with a length and gives back a boolean, not the value, so nothing needs connecting. Line 5 prints true: "Asha" has 4 letters, more than 3. Line 6 prints false: the cart has 2 items.

The table below lists the three ways a constraint asks for too much. Read each row as: what you wrote, what goes wrong, and what to write.

Asking for too much, and the fix
When you writeWhat goes wrongWrite instead
T extends { id: number; name: string }, but the function only reads idOrders with no name are turned away, though the function never reads a name.T extends { id: number }: ask only for what you use.
L extends any[] for the whole listlist[0] is any, so the item's type is lost.list: T[], so T is the type of one item.
T extends { length: number } with T used only onceT connects nothing, so it adds nothing but reading time.value: { length: number }, a plain parameter type.

A product table sorted by any column

A product table on an admin page sorts by whichever column header you press. One generic sortBy works for any table, and only accepts real column names:

Sort rows by a column name (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number }; function sortBy<T, K extends keyof T>(rows: T[], key: K): T[] {  return [...rows].sort((a, b) => (a[key] < b[key] ? -1 : 1));} const products: Product[] = [  { name: "Tea", price: 3 },  { name: "Cake", price: 4 },  { name: "Bun", price: 2 },];const byPrice = sortBy(products, "price");//    ^?console.log(byPrice.map((product) => product.name));sortBy(products, "colour");

Line 3 declares T for a row and K extends keyof T for the column. Line 4 copies the rows with [...rows], so the original array stays as it was, and sorts the copy by that column, smaller values first (the Sorting arrays lesson covers sort). Lines 7 to 11 are three products. Line 12 sorts them by price, and line 13 shows const byPrice: Product[]. Line 14 prints [ 'Bun', 'Tea', 'Cake' ]: 2, 3, then 4.

Line 15 is a column header someone misspelled, or a column that was removed. TypeScript marks it on purpose: Argument of type '"colour"' is not assignable to parameter of type 'keyof Product'. If that call ran, every a[key] would be undefined, so the sort would compare nothing. Without K extends keyof T, nothing would tell you.

Error or fine?
  • function longest<T extends { length: number }>(a: T, b: T) { return a.length >= b.length ? a : b; } longest(3, 4);
  • function longest<T extends { length: number }>(a: T, b: T) { return a.length >= b.length ? a : b; } longest([1], [2, 3]);
  • function size<T>(value: T) { return value.length; }
  • function size<T extends string | unknown[]>(value: T) { return value.length; }
  • function get<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } get({ tea: 3 }, "cake");
  • function get<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } get({ tea: 3 }, "tea");
  • function idOf<T extends { id: number }>(item: T) { return item.id; } idOf({ id: 7, total: 9 });
  • function idOf<T extends { id: number }>(item: T) { return item.id; } idOf({ id: "7" });
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

You have now seen extends in three places, and only one of them is a constraint. The table below compares them. Read each row as: what you write, what it means, and whether it makes something new. (A fourth use, T extends U ? X : Y, asks a question about a type; Conditional types covers it later.)

Three meanings of extends
When you writeWhat it meansDoes it make something new?
class Admin extends UserAdmin is a new class that gets everything User has, and can add more.Yes. A class you can create objects with.
interface Admin extends UserThe type Admin has everything User has, plus what it adds.Yes. A new type with a name.
<T extends User>T may be any type that fits User: it has at least what User has.No. It only limits what T may be.
  • "T extends User means T must be a class that inherits from User." No. Any type that fits User is allowed, even a plain object. Nothing is inherited.
  • "Inside the function, T is the constraint type." No. The constraint is the least T has. A caller can fill in something more specific, like string[], so a new { length: 3 } isn't a T.
  • "A constraint makes the result lose its type." No. T stays the exact type of the argument. Plain parameter types like a: { length: number } lose it.
  • "Asking for more properties is safer." No. Every extra property turns away values that would have worked. Ask for what the function uses.
  • "key: string is fine for a property name." No. Any text is a string, typos included. K extends keyof T allows only the object's real names, and tells TypeScript which one you passed.
  • "Constraints are checked while the code runs." No. Like every type, they're removed before the code runs. They only stop wrong calls while you write them.

Practice exercises

Each exercise is TypeScript. When you change code, press Edit & run on the starter code: the errors and ^? answers update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upFind the call TypeScript marks

Three calls to longest. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

const tags = longest(["sale"], ["new"]);
const sizes = longest({ length: 2 }, { length: 3 });
const totals = longest(9, 12);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead the type of one setting

    What type does TypeScript give tips? Guess first. Then point at tips, or press Edit & run and add a // ^? line under it.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Settings = { theme: string; fontSize: number; showTips: boolean }; const settings: Settings = { theme: "dark", fontSize: 16, showTips: true }; function getSetting<K extends keyof Settings>(key: K): Settings[K] {  return settings[key];} const tips = getSetting("showTips");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd the constraint shortest needs

      shortest gives back the shorter of two lists, but TypeScript marks line 2. Add a constraint so the errors go away. What does the code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function shortest<T>(a: T, b: T): T {  return a.length <= b.length ? a : b;} console.log(shortest(["Tea", "Cake"], ["Pie"]));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAllow only real column names

        pluck makes a list of one column, like every price. TypeScript marks line 4: any text, as a string, can't be used to look up a property of a T. Fix it with a second type parameter. What does the code print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Product = { name: string; price: number }; function pluck<T>(items: T[], key: string) {  return items.map((item) => item[key]);} const products: Product[] = [  { name: "Tea", price: 3 },  { name: "Cake", price: 4 },];console.log(pluck(products, "price"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeLoosen a constraint that turns orders away

          TypeScript turns the orders away on line 8. Change only what the constraint asks for, then print the order's total. What does it print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Order = { id: number; total: number }; function findById<T extends { id: number; name: string }>(items: T[], id: number) {  return items.find((item) => item.id === id);} const orders: Order[] = [{ id: 7, total: 9 }];const order = findById(orders, 7);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeTie a form value to its field

            TypeScript reports no errors, yet the age is wrong. What does the starter print? Then change value's type so TypeScript catches it, and fix the call.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type ProfileForm = { name: string; age: number; newsletter: boolean }; function updateField<T, K extends keyof T>(form: T, key: K, value: any): T {  return { ...form, [key]: value };} const form: ProfileForm = { name: "Asha", age: 30, newsletter: false };const next = updateField(form, "age", "31");console.log(next.age + 1);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask two things: what must T (or K) have, and what is it for this call? The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The generic constraints quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does T extends { length: number } mean on a type parameter?

                Choose an answer to see the explanation.

              2. Question 2 of 8Which line does TypeScript flag in these two calls to longest?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function longest<T extends { length: number }>(a: T, b: T): T {
                  return a.length >= b.length ? a : b;
                }
                
                const tags = longest(["sale"], ["sale", "new"]);
                const totals = longest(10, 20);

                Choose an answer to see the explanation.

              3. Question 3 of 8What type does age get from getProperty?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
                  return obj[key];
                }
                
                const user = { name: "Asha", age: 30 };
                const age = getProperty(user, "age");

                Choose an answer to see the explanation.

              4. Question 4 of 8What type does cart get from this version with no type parameter?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function longest(a: { length: number }, b: { length: number }) {
                  return a.length >= b.length ? a : b;
                }
                
                const cart = longest(["Tea"], ["Tea", "Cake"]);

                Choose an answer to see the explanation.

              5. Question 5 of 8What type does price get when the constraint covers the whole list?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function firstOfList<L extends any[]>(list: L) {
                  return list[0];
                }
                
                const price = firstOfList([3, 4]);

                Choose an answer to see the explanation.

              6. Question 6 of 8What does this sorted table print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function sortBy<T, K extends keyof T>(rows: T[], key: K): T[] {
                  return [...rows].sort((a, b) => (a[key] < b[key] ? -1 : 1));
                }
                
                const products = [
                  { name: "Tea", price: 3 },
                  { name: "Bun", price: 2 },
                ];
                console.log(sortBy(products, "price").map((product) => product.name).join(", "));

                Choose an answer to see the explanation.

              7. Question 7 of 8A function only reads item.id. What should its constraint ask for?

                Choose an answer to see the explanation.

              8. Question 8 of 8Why can't a function that returns T give back a new { length: minimum } object?

                Choose an answer to see the explanation.

              Key takeaways

              • Without a constraint, T could be any type, so you can't read .length or any property on it.
              • T extends { length: number } allows any type with at least a numeric length, and T stays the exact type of the argument.
              • keyof Settings is the list of a type's property names. K extends keyof T allows only real names, and T[K] is that property's type.
              • One type parameter can appear in another's constraint, which ties a key to its object and a value to its key.
              • A constraint is a minimum. A function that returns T can't give back something that only matches the constraint.
              • Ask for what the function uses, keep the type parameter as simple as possible, and skip it when it would appear only once.

              Remember the one-liner.
              A constraint says what every T has at least, so you can use it, while each call keeps its own exact type.

              Coming next: Generic types & interfaces. It puts type parameters on types themselves, like ApiResponse<T>.

              CompleteFrontend Clear concepts. Working examples.