cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Variance & assignability

Learn when a list, a loader, or a click handler of one type can stand in for another, why methods and arrays are looser, and what in and out mean.

By the end, you can
  • 01
    Predict which way things fitSay whether a loader, a handler, or a list of a subtype fits where one of the wider type goes, and why inputs flip the direction.
  • 02
    Spot the loose spotsExplain why methods and arrays let some unsafe code through, and use function-type properties and readonly arrays to close the gap.
  • 03
    Read and write in and outRead variance annotations on type parameters, and know when they help and why a wrong one hides bugs.

One type standing in for another

A shop has plain products and products on sale. A sale product has everything a product has, a name and a price, plus a sale price. Can a sale product go where the code expects a product? And the other way round? Here is what TypeScript says:

A sale product where a product goes (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; function showPrice(product: Product) {  console.log(`${product.name}: $${product.price}`);} const cake: SaleProduct = { name: "Cake", price: 4, salePrice: 3 };showPrice(cake); const tea: Product = { name: "Tea", price: 3 };const teaOnSale: SaleProduct = tea;

Lines 1 and 2 describe the two types. Line 4's showPrice takes a Product. Line 8 makes a sale cake, and line 9 passes it to showPrice. That's fine: showPrice only reads a name and a price, and the cake has both. It prints Cake: $4.

Line 12 goes the other way: it stores a plain tea where a SaleProduct goes. TypeScript marks it, on purpose here: Property 'salePrice' is missing in type 'Product' but required in type 'SaleProduct'. In plain words: code that expects a sale product may read salePrice, and tea doesn't have one.

When a value of one type can go wherever another type is expected, TypeScript calls it assignable to that type, and the first type is a subtype of the second. The second, wider type is the supertype. SaleProduct is a subtype of Product. TypeScript decides this by comparing shapes, as Structural typing showed. Subtypes chain together, from specific to general:

Two rows of types joined by arrows, from more specific on the left to more general on the right. Top row: SaleProduct, then Product, then an object with just a name. Bottom row: the literal type "sale", then string, then unknown.more specificmore generalSaleProductProduct{ name: string }"sale"stringunknown
Each arrow means "fits where the next type goes". A SaleProduct fits where a Product goes, and a Product fits where { name: string } goes. Never the other way.

The table below lists subtype pairs you meet in frontend code. Read each row as: a value of this type fits where that type goes, because…

Subtypes you already know
The subtypeFits where this goesWhy
SaleProductProductIt has a name and a price, plus a sale price.
"sale"stringThe literal type "sale" holds one string, so every value of it is a string.
MouseEventEventThe browser's MouseEvent has every property an Event has, plus the pointer's position.
stringstring | numberA union accepts a value of each of its members.
Any typeunknownunknown accepts every value.

That covers single values. This lesson asks the next question: what about types built from other types? Is a list of sale products a list of products? Is a function that gives back a sale product a function that gives back a product? Is a click handler for sale products a handler for products? The answer depends on whether the type inside goes in, comes out, or both. That rule has a name: variance.

Real-life analogyFruit in, fruit out

A friend asks you for a piece of fruit. An apple is fine, because every apple is a fruit. Next, they ask for a bowl to take fruit from. A bowl of apples works, because everything they take out is a fruit. Then they ask for someone to finish the leftover fruit, any kind. Someone who only eats apples won't do: they might be handed a banana. Someone who eats any food will.

In real life: An apple is a fruit
In TypeScript: A SaleProduct is a Product
In real life: A bowl of apples works when your friend only takes fruit out
In TypeScript: () => SaleProduct fits where () => Product goes
In real life: Someone who eats any food can finish any fruit
In TypeScript: (p: Product) => void fits where (p: SaleProduct) => void goes
In real life: Someone who only eats apples gets handed a banana
In TypeScript: A SaleProduct handler is given a plain Product
The main idea

Variance says whether a type built from another type, like a list, a loader, or a handler, follows the "fits" direction of the type inside it. What comes out keeps the direction. What goes in flips it. What goes both in and out must match exactly.

Outputs keep the direction: covariance

Start with something that only hands values out. A loader is a function you call to get a product, the way a page loads one from a server. Loader<T> is a generic type: the blank T says what the loader gives back.

A sale loader where a product loader goes (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number };type Loader<T> = () => T; const loadSale: Loader<SaleProduct> = () => ({ name: "Cake", price: 4, salePrice: 3 });const loadProduct: Loader<Product> = loadSale;console.log(loadProduct().name); const loadAnySale: Loader<SaleProduct> = loadProduct;

Line 3 says a Loader<T> is a function that takes nothing and returns a T. Line 5 makes a loader of sale products. Line 6 stores it where a loader of products goes, and that's fine: whoever calls it expects a product and gets a sale product, which has everything a product has. Line 7 prints Cake.

Line 9 goes the other way, and TypeScript marks it on purpose: Type 'Loader<Product>' is not assignable to type 'Loader<SaleProduct>'. Its second line gives the reason: Property 'salePrice' is missing. Code that wants sale products would get a product with no sale price.

So Loader follows the direction of its T: SaleProduct fits where Product goes, and Loader<SaleProduct> fits where Loader<Product> goes. A type that keeps the direction is called covariant ("co" means "together": the two move together). Anything that only hands values out works this way:

Things that only hand values outPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; const saleItems: readonly SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];const items: readonly Product[] = saleItems; const saleReply: Promise<SaleProduct> = Promise.resolve(saleItems[0]);const reply: Promise<Product> = saleReply;console.log(items[0].name);

Line 4 makes a readonly list of sale products: a list nobody can push into or change through that name (Arrays and tuples covers readonly). Line 5 stores it as a read-only list of products. Line 7 makes a Promise of a sale product, and line 8 stores it as a promise of a product. Both are fine: you only read from them. Line 9 prints Cake.

Inputs flip the direction: contravariance

Now something that takes values in: a handler, the function a list calls when the shopper selects a product. The list decides what to pass. The handler only decides what it can accept. Try different handlers below:

Playground: which handler fits?
A list that calls a select handlerPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number };type SelectHandler = (product: Product) => void; function renderList(products: Product[], onSelect: SelectHandler) {  onSelect(products[0]);} const handler = (item: Product) => console.log(item.price);renderList([{ name: "Tea", price: 3 }], handler);
Console output3
What happens
Handler takesProduct
TypeScriptNo errors
Try it yourself

The handler takes a Product, exactly what renderList gives it on line 6. It reads item.price, and the code prints 3.

TypeScript checks each version for real, and the output is what the code prints. The buttons change only line 9. Reset goes back to item: Product, with strictFunctionTypes on.

Line 3 describes a SelectHandler: a function that takes a Product. Line 5's renderList takes a list and a handler, and line 6 calls the handler with the first product. Line 9 is the handler, and line 10 passes it in.

  • item: Product matches exactly. It prints 3.
  • item: { name: string } asks for less than a product. Every product has a name, so this handler can deal with anything the list passes. No error, and it prints Tea.
  • item: SaleProduct asks for more. TypeScript marks line 10: Argument of type '(item: SaleProduct) => void' is not assignable to parameter of type 'SelectHandler'. It means "this handler can't take everything the list may give it". Run it anyway, and the list passes tea, which has no sale price: it prints undefined.

For values and outputs, the narrower type fits. For inputs it's the other way: the handler with the wider parameter fits. (item: { name: string }) => void fits where (product: Product) => void goes, even though Product fits where { name: string } goes. The direction flips, so function parameters are called contravariant ("contra" means "against").

Three rows, each an arrow meaning fits where. Values: SaleProduct fits where Product goes. Outputs, the same way: a function returning SaleProduct fits where one returning Product goes. Inputs, flipped: a function taking Product fits where one taking SaleProduct goes.valuesSaleProductProductoutputs: the same way() => SaleProduct() => Productinputs: flipped(p: Product) => void(p: SaleProduct) => void
Read each arrow as "fits where". For values and for what a function gives back, the arrow points the same way. For what a function takes in, it flips: the handler for any Product fits where a SaleProduct handler goes.

The checkbox in the playground turns off strictFunctionTypes, a check that strict turns on (tsconfig strictness lists it). Without it, TypeScript checks parameters both ways: a handler fits if its parameter fits the other one in either direction. This is called bivariance. Choose item: SaleProduct and untick the box: the error disappears, and the code still prints undefined. Keep strict on.

The browser's own events follow the same rule. A handler that accepts any Event fits every listener:

One handler for every kind of eventPop out in the code editor (opens in a new tab)TypeScript
function logEvent(event: Event) {  console.log(`${event.type} happened`);} const button = document.createElement("button");button.addEventListener("click", logEvent);button.addEventListener("keydown", logEvent);button.click();

Line 1's logEvent takes any Event. Line 6 uses it for clicks, where TypeScript knows the listener gets a MouseEvent. Line 7 uses it for key presses, which pass a KeyboardEvent. Both fit, because both are subtypes of Event, and logEvent only reads event.type. Line 8 clicks the button, and line 2 prints click happened. A handler that needs a MouseEvent would not fit the key listener (Typing events covers listeners).

Methods are checked both ways

strictFunctionTypes has one gap. An object type can describe a function member in two ways, and TypeScript checks them differently:

A method and a function property (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; interface MethodProps { onSelect(product: Product): void }interface PropertyProps { onSelect: (product: Product) => void } const logSale = (item: SaleProduct) => console.log(item.salePrice);const withMethod: MethodProps = { onSelect: logSale };const withProperty: PropertyProps = { onSelect: logSale };withMethod.onSelect({ name: "Tea", price: 3 });

Line 4 writes onSelect as a method: a name, brackets, and a return type. Line 5 writes it as a property whose type is a function type (Function types covers them). Line 7's logSale needs a sale product.

  • Line 8 stores logSale as the method. No error: methods are still checked both ways, even with strict on.
  • Line 9 stores the same function as the property. TypeScript marks it, on purpose: Type '(item: SaleProduct) => void' is not assignable to type '(product: Product) => void'.
  • Line 10 calls the method with tea, and it prints undefined. Line 8 let the bug through.

Why leave methods out? The TypeScript team did it on purpose when strictFunctionTypes arrived, so that types with methods, like Array<T>, keep fitting the way people expect. The next section shows what that costs. For your own types, the fix is simple: write callbacks as properties with function types, like line 5, and you get the strict check.

The table below sums up the three cases. Read each row as: written this way, checked this way, so a sale-product handler gets…

How TypeScript checks a parameter
How the member is writtenHow its parameter is checkedA SaleProduct handler where a Product handler goes
A property with a function type: onSelect: (product: Product) => voidInputs flip: the handler must accept every Product.An error, like line 9 of the example.
A method: onSelect(product: Product): voidBoth ways, even with strict on. One direction is enough.No error, like line 8 of the example.
Any function type, with strictFunctionTypes offBoth ways, like a method.No error, and the bug gets through.

Arrays and the unsafe write

A list of sale products fits where a list of products goes. That's handy for code that only reads the list. But a normal array can also be written to:

A push through the wider namePop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; const saleItems: SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];const items: Product[] = saleItems;items.push({ name: "Tea", price: 3 }); for (const item of saleItems) {  console.log(`${item.name}: $${item.salePrice}`);}

Line 4 makes a list of sale products. Line 5 stores it as Product[], and TypeScript allows it. Line 6 pushes tea, which is a fine Product. Lines 8 to 10 print each sale price: Cake: $3, then Tea: $undefined. There are no errors, and the sale list now holds something that isn't a sale product.

The reason is that line 5 doesn't copy the array. items and saleItems are two names for the same array (Objects and references explains this). Step through it:

Step through the unsafe push
Step 0 of 5Ready
Your turn: follow the blue line

This replays the example with its types removed, which is the code that runs. Watch line 3 change the array that both names point at.

Running in
  1. script
Next: line 1
const items = saleItems;items.push({ name: "Tea", price: 3 }); for (const item of saleItems) {  console.log(`${item.name}: $${item.salePrice}`);}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Two tags, saleItems typed SaleProduct[] and items typed Product[], both point at one array. The array holds the cake, with a sale price of 3, and tea, pushed through items, with no sale price.saleItemsSaleProduct[]itemsProduct[]Cake, sale 3Teano salePriceone array
Two names, one array. Line 5 makes items a second name for saleItems. Line 6 pushes tea through items, so the sale list now holds a product with no salePrice.

Arrays are treated as covariant even though they can be written to. That's unsound: TypeScript lets through code that can put a wrong value in a list. It's a choice that keeps everyday code easy, and object properties work the same way:

A property written through the wider namePop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; const saleSlot: { item: SaleProduct } = { item: { name: "Cake", price: 4, salePrice: 3 } };const slot: { item: Product } = saleSlot;slot.item = { name: "Tea", price: 3 };console.log(saleSlot.item.salePrice);

Line 4 makes an object holding a sale cake. Line 5 stores it as an object holding any product. Line 6 puts tea in through slot, and line 7 prints undefined: the sale slot holds tea now. Again, no errors.

When code only needs to read a list, say so with readonly. A read-only list of sale products still fits where a read-only list of products goes, and the unsafe write becomes an error:

A read-only list blocks the push (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; const saleItems: SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];const items: readonly Product[] = saleItems;items.push({ name: "Tea", price: 3 });console.log(items.length);

Line 5 stores the sale list as readonly Product[], and that's fine. Line 6's push is now an error, on purpose: Property 'push' does not exist on type 'readonly Product[]'. A read-only list has no methods that change it. readonly only exists in types, though: run the code anyway and line 7 prints 2. The check is what protects you.

in and out: writing the variance down

TypeScript works out the variance of each generic type from how its body uses the type parameter. Since TypeScript 4.7, you can also write it down, with out and in before the type parameter:

Variance written on the type parametersPop out in the code editor (opens in a new tab)TypeScript
type Loader<out T> = () => T;type Handler<in T> = (value: T) => void; interface Store<in out T> {  get: () => T;  set: (value: T) => void;}
  • Line 1: out T says T only comes out. A loader returns a T, so it's covariant.
  • Line 2: in T says T only goes in. A handler takes a T, so it's contravariant.
  • Line 4: in out T says both. Line 5's get hands a T out, and line 6's set takes one in, so a store is invariant: two stores fit only when they hold the same type.
A window titled Store<in out T>. Its first line, get: () => T, has an arrow out to the note T comes out: out. Its second line, set: (value: T) => void, has an arrow out to the note T goes in: in.Store<in out T>get: () => Tset: (value: T) => voidT comes out:outT goes in:in
get hands a T out, and set takes a T in. A type parameter used both ways is in out: the two stores must hold exactly the same type.

An annotation is a claim, and TypeScript checks it against the body. Try each one on a store of sale products, which line 14 tries to use as a store of products:

Playground: annotate the store
A store that reads and writesPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; interface Store<T> {  get: () => T;  set: (value: T) => void;} function makeStore<T>(value: T): Store<T> {  return { get: () => value, set: (next) => { value = next; } };} const saleStore = makeStore<SaleProduct>({ name: "Cake", price: 4, salePrice: 3 });const productStore: Store<Product> = saleStore;productStore.set({ name: "Tea", price: 3 });console.log(saleStore.get().salePrice);
Line 4 saysStore<T>
What happens
TypeScript1 error: line 14
Line 14 caught?Yes
Try it yourself

With no annotation, TypeScript works out the variance from the body. get reads a T and set writes one, so the two stores must match exactly. Line 14 gets Type 'Store<SaleProduct>' is not assignable to type 'Store<Product>', which is the bug: line 15 would put tea in the sale store.

TypeScript checks each version for real. The buttons change only line 4. Every version prints undefined when it runs, because the annotation is a type and is removed. Reset goes back to no annotation.

With no annotation, or with the true in out T, line 14 gets an error, which is right: line 15 puts tea in the sale store, and line 16 prints undefined. With out T, line 4 gets Type 'Store<sub-T>' is not assignable to type 'Store<super-T>' as implied by variance annotation. sub-T and super-T are stand-ins for "some subtype" and "its supertype"; the message means "your annotation says a store of a subtype fits where a store of its supertype goes, but the body disagrees". Look at line 14 with out T: it isn't marked any more, because TypeScript trusts the annotation when it compares two stores. A wrong annotation hides the real bug.

A few rules about annotations:

  • They go on type parameters of interfaces, classes, and type aliases for object or function types. A function's type parameter can't have one.
  • TypeScript checks each one against the body, as the playground showed, and reports one that lets through more than the body is safe for. A stricter one, like in out T on a type that only hands T out, is allowed: the type then fits less than it could.
  • They don't change what fits when TypeScript compares two shapes property by property. They're used as a shortcut when it compares two versions of the same generic type, like Store<SaleProduct> and Store<Product>.
  • You rarely need them. They help a reader see how a parameter is used, and in very large or self-referencing types they make checking faster and more exact. Library authors use them most.
A variance annotation on a function (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function firstItem<out T>(items: T[]): T {  return items[0];}

Line 1 puts out on a function's type parameter. TypeScript says, on purpose here: 'out' modifier can only appear on a type parameter of a class, interface or type alias.

Props for a product list

A product list component takes its data and a callback as props: one object with everything the component needs. Variance decides how to write that type so callers can pass what they have, and mistakes still get caught:

Props that accept sale products safelyPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; type ProductListProps = {  items: readonly Product[];  onSelect: (product: Product) => void;}; function renderProductList(props: ProductListProps) {  for (const item of props.items) console.log(`Show ${item.name}`);  props.onSelect(props.items[0]);} const saleItems: SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];renderProductList({ items: saleItems, onSelect: (item) => console.log(`Picked ${item.name}`) });//                                               ^?

Line 5 types items as readonly Product[]: the component only reads the list, so any list of products, sale products included, can go in, and nothing can push into it. Line 6 types onSelect as a property with a function type, so strictFunctionTypes checks the callbacks people pass. Line 15 passes the sale list and a callback. The query on line 16 shows TypeScript gives the callback's item the type Product, from the props type. It prints Show Cake, then Picked Cake.

A callback that needs too much (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; type ProductListProps = {  items: readonly Product[];  onSelect: (product: Product) => void;}; function renderProductList(props: ProductListProps) {  for (const item of props.items) console.log(`Show ${item.name}`);  props.onSelect(props.items[0]);} const saleItems: SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];const pickSale = (item: SaleProduct) => console.log(item.salePrice);renderProductList({ items: saleItems, onSelect: pickSale });

Lines 1 to 14 are the same. Line 15's pickSale needs a sale product, and line 16 passes it as onSelect. TypeScript marks line 16: Type '(item: SaleProduct) => void' is not assignable to type '(product: Product) => void'. The component promises only a Product, so a callback that needs a sale price doesn't fit. Had line 6 been a method, there would be no error.

Error or fine?
  • const teas: string[] = ["green"]; const drinks: (string | number)[] = teas;
  • const teas: readonly string[] = ["green"]; const copy: string[] = teas;
  • const show: (name: string) => void = (name: string | number) => console.log(name);
  • const show: (name: string | number) => void = (name: string) => console.log(name.length);
  • const load: () => string | number = () => "green";
  • const list: { pick(name: string | number): void } = { pick: (name: string) => {} };
  • const list: { pick: (name: string | number) => void } = { pick: (name: string) => {} };
  • type Box<out T> = { set: (value: T) => void };
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it, with strict on. 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

The table below sums up the three kinds of variance. Read each row as: where the type parameter appears, what that's called, and what fits.

Covariant, contravariant, and invariant
Where T appearsWhat it's calledWhat fits
Only in what comes out, like () => T, Promise<T>, or readonly T[]Covariant. The annotation is out T.Loader<SaleProduct> fits where Loader<Product> goes: the same way as the types inside.
Only in what goes in, like (value: T) => voidContravariant. The annotation is in T.Handler<Product> fits where Handler<SaleProduct> goes: the other way round.
In both, like a store with get and setInvariant. The annotation is in out T.Neither fits the other. The types inside must be the same.
  • "If a sale product fits, a list of sale products is always safe as a list of products." No. TypeScript allows it, but a push through the wider name puts a plain product in the sale list. Use readonly when you only read.
  • "A handler for sale products works wherever a product handler goes." No. Inputs flip. The caller may pass any product, so the handler must accept any product, or something wider.
  • "Methods and function properties are checked the same way." No. Methods are checked both ways even with strict. Write callbacks as properties with function types to get the strict check.
  • "in and out let me pick any variance I like." No. TypeScript reports one that lets through more than the body is safe for. And a wrong one can hide a bug: with out T on the store, line 14 stopped being marked.
  • "Variance is about classes and inheritance." No. TypeScript compares shapes. Any two types with matching shapes follow these rules, with or without extends.
  • "readonly stops the array from changing." No. It stops your code from changing it through that name. The types are removed before the code runs, and another name for the same array can still push.

Practice exercises

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

Exercise 1 · Warm-upFind the loader that doesn't fit

Two loaders and two assignments. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };
type SaleProduct = { name: string; price: number; salePrice: number };
type Loader<T> = () => T;

const loadTea: Loader<Product> = () => ({ name: "Tea", price: 3 });
const loadCake: Loader<SaleProduct> = () => ({ name: "Cake", price: 4, salePrice: 3 });

const first: Loader<Product> = loadCake;
const second: Loader<SaleProduct> = loadTea;

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

    Exercise 2 · Warm-upPredict a push through a wider name

    TypeScript reports no errors. What does this code print? Then make line 2 safe.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const teaNames: string[] = ["green"];const labels: (string | number)[] = teaNames;labels.push(4);console.log(teaNames.length, typeof teaNames[1]);

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

      Exercise 3 · PracticeFix a handler that asks for too much

      TypeScript marks line 9. Change the handler so it fits, and print the product's price. What does it print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number };type SelectHandler = (product: Product) => void; function renderList(products: Product[], onSelect: SelectHandler) {  onSelect(products[0]);} renderList([{ name: "Tea", price: 3 }], (item: SaleProduct) => console.log(item.salePrice));

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

        Exercise 4 · PracticeStop a function from writing into your list

        addFreeGift quietly puts a plain product into the sale list. Change it so it can't, and so it gives back a new list instead. What does your version print for both lengths?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; function addFreeGift(items: Product[]) {  items.push({ name: "Gift card", price: 0 });} const saleItems: SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];addFreeGift(saleItems);console.log(saleItems.length);

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

          Exercise 5 · ChallengeAnnotate three generic types

          Add in, out, or in out to each type parameter. TypeScript checks your claims as you type. Which one does Cache need?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          interface Feed<T> { latest: () => T }interface Sink<T> { send: (value: T) => void }interface Cache<T> { read: () => T; write: (value: T) => void }

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

            Exercise 6 · ChallengeTurn a method into a strict callback

            TypeScript is happy with this code. What does it print? Then rewrite line 4 so TypeScript catches the bug, and fix the handler.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; interface ProductListProps { onSelect(product: Product): void } const props: ProductListProps = {  onSelect: (item: SaleProduct) => console.log(item.salePrice),};props.onSelect({ name: "Tea", price: 3 });

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

              Check your understanding

              For each question, ask which way the value flows: out of a type, into it, or both. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The variance quiz · 7 questionsScore: first tries count
              1. Question 1 of 7SaleProduct has every property of Product, plus salePrice. Which sentence is true?

                Choose an answer to see the explanation.

              2. Question 2 of 7This code has no type errors. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Tea = { name: string };
                type GreenTea = { name: string; leaf: string };
                
                const greenSlot: { tea: GreenTea } = { tea: { name: "Sencha", leaf: "whole" } };
                const slot: { tea: Tea } = greenSlot;
                slot.tea = { name: "Chai" };
                console.log(greenSlot.tea.leaf);

                Choose an answer to see the explanation.

              3. Question 3 of 7A list calls onClick with a MouseEvent. Which handler fits where (event: MouseEvent) => void goes?

                Choose an answer to see the explanation.

              4. Question 4 of 7Which line of this handler code does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type PageEvent = { type: string };
                type ClickEvent = { type: string; x: number };
                
                interface WithMethod { handle(event: PageEvent): void }
                interface WithProperty { handle: (event: PageEvent) => void }
                
                const onClick = (event: ClickEvent) => console.log(event.x);
                const first: WithMethod = { handle: onClick };
                const second: WithProperty = { handle: onClick };

                Choose an answer to see the explanation.

              5. Question 5 of 7What does strictFunctionTypes change?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does TypeScript say about this annotation?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                interface Field<out T> {
                  value: () => T;
                  update: (next: T) => void;
                }

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does TypeScript let a SaleProduct[] go where a Product[] goes, even though a push could break it?

                Choose an answer to see the explanation.

              Key takeaways

              • A subtype has everything its supertype has, so it fits where the supertype goes: a SaleProduct where a Product goes.
              • Types that only hand values out, like loaders, promises, and read-only arrays, are covariant: they keep the direction.
              • Function parameters are contravariant: a handler for the wider type fits where a handler for the narrower type goes.
              • strictFunctionTypes, part of strict, checks function-type parameters strictly. Methods are still checked both ways.
              • Arrays and object properties are treated as covariant even though you can write to them. Use readonly when you only read.
              • out, in, and in out write the variance down. TypeScript checks them, and you rarely need them.

              Remember the one-liner.
              What comes out keeps the direction, what goes in flips it, and what goes both ways must match.

              Coming next: Decorators. They are functions that wrap a class's methods and fields, like a logger or an auto-bound click handler, and TypeScript checks where you put them.

              CompleteFrontend Clear concepts. Working examples.