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.
- 01Predict 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.
- 02Spot the loose spotsExplain why methods and arrays let some unsafe code through, and use function-type properties and readonly arrays to close the gap.
- 03Read 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:
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:
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…
| The subtype | Fits where this goes | Why |
|---|---|---|
SaleProduct | Product | It has a name and a price, plus a sale price. |
"sale" | string | The literal type "sale" holds one string, so every value of it is a string. |
MouseEvent | Event | The browser's MouseEvent has every property an Event has, plus the pointer's position. |
string | string | number | A union accepts a value of each of its members. |
| Any type | unknown | unknown 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.
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
SaleProductis aProduct - In real life: A bowl of apples works when your friend only takes fruit out
- In TypeScript:
() => SaleProductfits where() => Productgoes - In real life: Someone who eats any food can finish any fruit
- In TypeScript:
(p: Product) => voidfits where(p: SaleProduct) => voidgoes - In real life: Someone who only eats apples gets handed a banana
- In TypeScript: A
SaleProducthandler is given a plainProduct
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.
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:
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:
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);3ProductNo errorsThe handler takes a Product, exactly what renderList gives it on line 6. It reads item.price, and the code prints 3.
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 printsundefined.
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").
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:
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:
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
logSaleas the method. No error: methods are still checked both ways, even withstricton. - 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 the member is written | How its parameter is checked | A SaleProduct handler where a Product handler goes |
|---|---|---|
A property with a function type: onSelect: (product: Product) => void | Inputs flip: the handler must accept every Product. | An error, like line 9 of the example. |
A method: onSelect(product: Product): void | Both ways, even with strict on. One direction is enough. | No error, like line 8 of the example. |
Any function type, with strictFunctionTypes off | Both 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:
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:
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.
script
const items = saleItems;items.push({ name: "Tea", price: 3 }); for (const item of saleItems) { console.log(`${item.name}: $${item.salePrice}`);}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:
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:
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:
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 TsaysTonly comes out. A loader returns aT, so it's covariant. - Line 2:
in TsaysTonly goes in. A handler takes aT, so it's contravariant. - Line 4:
in out Tsays both. Line 5'sgethands aTout, and line 6'ssettakes one in, so a store is invariant: two stores fit only when they hold the same type.
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:
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);Store<T>1 error: line 14YesWith 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.
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 Ton a type that only handsTout, 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>andStore<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.
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:
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.
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.
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 };
Sort each line by what TypeScript says about it, with strict on. Each line is checked on its own.
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.
| Where T appears | What it's called | What 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) => void | Contravariant. The annotation is in T. | Handler<Product> fits where Handler<SaleProduct> goes: the other way round. |
In both, like a store with get and set | Invariant. 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
readonlywhen 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 Ton 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.
Two loaders and two assignments. Which line will TypeScript mark?
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;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; // fine: a sale product is a productconst second: Loader<SaleProduct> = loadTea; // line 9: tea has no salePriceLine 8 is fine: a sale loader fits where a product loader goes. Line 9 stores the tea loader where a sale loader goes, and tea has no salePrice, so TypeScript marks line 9.
TypeScript reports no errors. What does this code print? Then make line 2 safe.
const teaNames: string[] = ["green"];const labels: (string | number)[] = teaNames;labels.push(4);console.log(teaNames.length, typeof teaNames[1]);const teaNames: string[] = ["green"];const labels: readonly (string | number)[] = teaNames;console.log(teaNames.length, labels.length);The starter prints 2 number: the push on line 3 lands in teaNames too, so a list of strings holds a number. Making labels read-only, as here, turns that push into an error, so the list keeps one string and this version prints 1 1.
TypeScript marks line 9. Change the handler so it fits, and print the product's price. What does it print?
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));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: Product) => console.log(item.price));A handler must accept everything the caller may pass. With item: Product it fits SelectHandler, the error on line 9 is gone, and it prints tea's price, 3.
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?
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);type Product = { name: string; price: number };type SaleProduct = { name: string; price: number; salePrice: number }; function withFreeGift(items: readonly Product[]): Product[] { return [...items, { name: "Gift card", price: 0 }];} const saleItems: SaleProduct[] = [{ name: "Cake", price: 4, salePrice: 3 }];const cartItems = withFreeGift(saleItems);console.log(saleItems.length, cartItems.length);With readonly Product[], a push would be an error, so the function builds a new array instead. The sale list keeps its one item, and the new cart list has two: it prints 1 2.
Add in, out, or in out to each type parameter. TypeScript checks your claims as you type. Which one does Cache need?
interface Feed<T> { latest: () => T }interface Sink<T> { send: (value: T) => void }interface Cache<T> { read: () => T; write: (value: T) => void }interface Feed<out T> { latest: () => T }interface Sink<in T> { send: (value: T) => void }interface Cache<in out T> { read: () => T; write: (value: T) => void }Feed hands a T out, so out T. Sink takes one in, so in T. Cache reads and writes, so in out T. TypeScript accepts all three, because each matches the body.
TypeScript is happy with this code. What does it print? Then rewrite line 4 so TypeScript catches the bug, and fix the handler.
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 });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: Product) => console.log(item.price),};props.onSelect({ name: "Tea", price: 3 });The starter prints undefined: tea has no sale price, and the method let the handler through. As a property, onSelect is checked strictly, so line 7 gets an error until the handler takes a Product. Then it prints 3.
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.
Question 1 of 7
SaleProducthas every property ofProduct, plussalePrice. Which sentence is true?Choose an answer to see the explanation.
Question 2 of 7This code has no type errors. What does it print?
Read the code, then predicttype 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.
Question 3 of 7A list calls
onClickwith aMouseEvent. Which handler fits where(event: MouseEvent) => voidgoes?Choose an answer to see the explanation.
Question 4 of 7Which line of this handler code does TypeScript flag?
Read the code, then predicttype 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.
Question 5 of 7What does
strictFunctionTypeschange?Choose an answer to see the explanation.
Question 6 of 7What does TypeScript say about this annotation?
Read the code, then predictinterface Field<out T> { value: () => T; update: (next: T) => void; }Choose an answer to see the explanation.
Question 7 of 7Why does TypeScript let a
SaleProduct[]go where aProduct[]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
SaleProductwhere aProductgoes. - 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 ofstrict, 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
readonlywhen you only read. out,in, andin outwrite 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.