ReturnType, Awaited & more
Use TypeScript's Exclude, Extract, NonNullable, ReturnType, Parameters, and Awaited to build new types from unions, functions, and promises you already have.
- 01Filter a unionTake members out of a union with
Exclude, keep only some withExtract, and dropnullandundefinedwithNonNullable. - 02Read a function's typesGet what a function returns with
ReturnTypeand what it takes withParameters, usingtypeofto turn the function into a type. - 03Name the data a promise givesUnwrap a promise with
Awaited, and combine it asAwaited<ReturnType<typeof f>>for an async function.
New types from the types you have
A shop tracks every order with a status: pending, paid, shipped, or cancelled. A reminder email should only go out for orders that are still open, pending or paid. You could write a second list of statuses by hand, but then two lists must be kept the same forever. TypeScript can make the new type from the one you already have:
type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";type OpenStatus = Exclude<OrderStatus, "shipped" | "cancelled">;// ^? function remindShopper(status: OpenStatus) { console.log(`Your order is ${status}`);}remindShopper("paid");remindShopper("cancelled");Line 1 lists the four statuses as a union: a type that allows one of several values, written with | between them (the Union types lesson covers them). Each value in the list is called a member. Line 2 makes OpenStatus with Exclude, which takes two types in angle brackets: the union to start from, and the members to take out. The query on line 3 shows the result: type OpenStatus = "pending" | "paid".
Lines 5 to 7 are a function that only takes an open status. Line 8 passes "paid", which is fine. Line 9 passes "cancelled", and TypeScript marks it, on purpose:
Argument of type '"cancelled"' is not assignable to parameter of type 'OpenStatus'means ""cancelled"isn't one of the valuesOpenStatusallows". An argument is a value you pass in a call.
When the code runs, it prints Your order is paid and then Your order is cancelled: types are removed before the code runs, so the error is a warning for you, not a stop.
Exclude goes through the four members of OrderStatus and takes out the ones that fit "shipped" | "cancelled". Two are left, and they make OpenStatus.Exclude is one of TypeScript's utility types: type helpers that come with TypeScript, ready to use in any file. The Partial, Pick, Omit & friends lesson covered the ones for object types. This lesson covers six more, for unions, functions, and promises:
ExcludeandExtracttake members out of a union, or keep only some.NonNullabletakes outnullandundefined.ReturnTypeandParametersread what a function gives back and what it takes.Awaitedgives the value inside a promise.
You pour a pot of soup through a sieve. Some days you want the broth, so you keep what runs through. Other days you want the vegetables, so you keep what the sieve catches. Same pot, same sieve, two different results, and the recipe stays as it was.
- In real life: A pot of soup with vegetables in it
- In TypeScript: A union, like
OrderStatus - In real life: A sieve with holes of one size
- In TypeScript: The second type, like
"shipped" | "cancelled" - In real life: Keep the broth that runs through
- In TypeScript:
Excludekeeps the members that don't fit the second type - In real life: Keep the vegetables the sieve catches
- In TypeScript:
Extractkeeps the members that do fit - In real life: The recipe card for the soup is unchanged
- In TypeScript:
OrderStatusstays as it was; you get a new type
A utility type is a built-in type helper. You give it a type in angle brackets, like Exclude<OrderStatus, "cancelled">, and it gives back a new type. The type you started from doesn't change, and when it does change later, the new type follows.
Filtering a union with Exclude and Extract
Exclude<Union, Remove> goes through Union one member at a time. For each member, it asks: does this fit Remove? "Fits" means the value would be allowed where Remove is the type; TypeScript's word for it is assignable. Every member that fits is taken out. Type your own second type on line 3, and watch the answer after ^? on line 4:
type OrderStatus = "pending" | "paid" | "shipped" | "cancelled"; type Result = Exclude<OrderStatus, "cancelled">;// ^?const next: Result = "paid";console.log(next);"cancelled""pending" | "paid" | "shipped"No errorsExclude takes "cancelled" out of OrderStatus, and the answer after ^? lists the three that are left. "paid" is one of them, so line 5 is fine.
With "shipped" | "cancelled", two statuses are left, as in the overview. With "paid", line 5 gets an error, because "paid" was taken out. With "canceled", spelled with one l, nothing is taken out and TypeScript says nothing: Exclude doesn't check that the second type is part of the union. With string, every status fits and is taken out, so nothing is left. TypeScript calls that never, the type with no values at all. cancelled without quotes gets Cannot find name 'cancelled', because a name without quotes means a type with that name.
Extract<Union, Keep> is the opposite. It keeps only the members that fit Keep. It's most useful with a union of object types, like the events a shopping cart sends when something changes:
type CartEvent = | { type: "add"; name: string; price: number } | { type: "remove"; name: string } | { type: "clear" }; type AddEvent = Extract<CartEvent, { type: "add" }>;// ^?type OtherEvent = Exclude<CartEvent, { type: "add" }>;// ^? function showAdded(event: AddEvent) { console.log(`Added ${event.name} for $${event.price}`);}showAdded({ type: "add", name: "Tea", price: 3 });Lines 1 to 4 describe three kinds of event. Each has a type property that says which kind it is (the Discriminated unions lesson covers this pattern). Line 6 keeps the members that fit { type: "add" }. An object type fits when it has at least those properties, so only the add event does. The query on line 7 shows it, with its name and price too.
Line 8 uses Exclude with the same check, and the query on line 9 shows the other two events. Lines 11 to 13 are a function that only takes add events, so it can read event.price: every AddEvent has one. Line 14 prints Added Tea for $3.
{ type: "add" }, splits CartEvent in two. Extract keeps the one event that fits it. Exclude keeps the other two.The table below puts the two helpers side by side. Read each row as: what you write, which members it keeps, and the result.
| When you write | What it keeps | The result |
|---|---|---|
Exclude<OrderStatus, "shipped" | "cancelled"> | The members that don't fit the second type. | "pending" | "paid" |
Extract<OrderStatus, "shipped" | "cancelled" | "refunded"> | The members that fit the second type. "refunded" isn't a member, so it doesn't appear. | "shipped" | "cancelled" |
Extract<CartEvent, { type: "add" }> | The object types that have at least type: "add". | { type: "add"; name: string; price: number; } |
Removing null and undefined with NonNullable
A value that might be missing often has a type like string | null. null means "nothing here, on purpose", and undefined means "not set" (see null and undefined). NonNullable<T> takes both out of T. Here a profile form has an optional nickname:
type ProfileForm = { name: string; nickname: string | null;}; type Nickname = NonNullable<ProfileForm["nickname"]>;// ^? function showBadge(nickname: Nickname) { console.log(`Hi, ${nickname.toUpperCase()}!`);} const form: ProfileForm = { name: "Asha", nickname: "Ash" };if (form.nickname) showBadge(form.nickname);showBadge(form.nickname);Lines 1 to 4 describe the form. The nickname can be left empty, so it's string | null. On line 6, ProfileForm["nickname"] means "the type of the nickname property in ProfileForm", which is string | null. NonNullable takes the null out, and the query on line 7 shows type Nickname = string.
Lines 9 to 11 show a badge. Line 10 can call toUpperCase, because a Nickname is always text. Line 13 fills in the form. Line 14 checks the nickname first: inside the if, it can't be null (the Narrowing lesson explains this). Line 15 doesn't check, and TypeScript marks it on purpose:
Argument of type 'string | null' is not assignable to parameter of type 'string'means "this nickname might benull, and the badge needs text".
This form has a nickname, so the code prints Hi, ASH! twice. With nickname: null, line 15 would throw a TypeError when it runs, because null has no toUpperCase.
NonNullable is most useful with a type parameter, where you don't know the exact type ahead of time (see Generic functions). This helper makes sure a value is there:
function requireValue<T>(value: T, label: string): NonNullable<T> { if (value === null || value === undefined) { throw new Error(`${label} is missing`); } return value;} const params = new URLSearchParams("city=Pune");const savedCity = params.get("city");// ^?const city = requireValue(savedCity, "city");// ^?console.log(city.toUpperCase());Line 1 says requireValue takes a value of any type T and a label, and gives back NonNullable<T>. Lines 2 to 4 throw an error when the value is null or undefined, so line 5 only runs with a real value, and TypeScript knows it. That's why return value fits NonNullable<T>.
Line 8 reads the part of a web address after the ?, here city=Pune, with URLSearchParams. Line 9 asks for the city. The query on line 10 shows string | null, because an address might not have one. Line 11 passes it through requireValue, and the query on line 12 shows const city: string. Line 13 prints PUNE.
Exclude, Extract, and NonNullable are one line each in TypeScript's own library:
type Exclude<T, U> = T extends U ? never : T;For each member ofT: if it fitsU, it becomesneverand drops out. If not, it stays.type Extract<T, U> = T extends U ? T : never;The same question, with the answers swapped.type NonNullable<T> = T & {};Here{}means any value exceptnullandundefined, so only the other parts ofTare kept.
The Conditional types lesson explains the ? : form. ReturnType, Parameters, and Awaited are written with infer, like the ReturnOf, ParamsOf, and UnwrapAll you wrote in The infer keyword. The next lesson writes helpers like these yourself.
A function's types with ReturnType and Parameters
The next two helpers read a function's type. They are the built-in versions of the ReturnOf and ParamsOf you wrote in The infer keyword. A helper takes a type, and a function is a value. The word typeof, written where a type goes, turns a value into its type (keyof & typeof covers it). typeof createCartItem means "the type of the function createCartItem": what it takes and what it gives back.
function createCartItem(name: string, price: number, quantity = 1) { return { name, price, quantity };} type CartItem = ReturnType<typeof createCartItem>;// ^?type ItemArgs = Parameters<typeof createCartItem>;// ^? const args: ItemArgs = ["Cake", 4];const cake: CartItem = createCartItem(...args);console.log(cake);Line 1 makes a cart item from a name, a price, and a quantity. The quantity is 1 unless you pass one: a default value (see Parameters). Line 2 returns the three as an object. TypeScript works out what the function returns, so nobody wrote that type down.
Line 5 names it with ReturnType, and the query on line 6 shows { name: string; price: number; quantity: number; }. Line 7 uses Parameters. The query on line 8 shows a tuple, a list with a fixed length and a type for each place: [name: string, price: number, quantity?: number | undefined]. The ? means the quantity may be left out.
Line 10 makes a list of arguments that fits ItemArgs, and line 11 spreads them into the call with ... (see Spread and rest). Line 12 prints { name: 'Cake', price: 4, quantity: 1 }.
Parameters reads what goes in: a name, a price, and an optional quantity. ReturnType reads what comes out: an object with all three. Both start from typeof createCartItem.Press the buttons to write line 5 five ways, and watch the answer after ^? and the list under the code:
function createCartItem(name: string, price: number, quantity = 1) { return { name, price, quantity };} type Result = ReturnType<typeof createCartItem>;// ^?{ name: string; price: number; quantity: number; }No errorsReturnType gives the type of what createCartItem returns: an object with a name, a price, and a quantity. You never wrote that type; TypeScript read it from line 2.
ReturnType and Parameters give the two types from the picture. Parameters, item 0 adds [0], which reads the first place of the tuple: string, the name. Without typeof gets 'createCartItem' refers to a value, but is being used as a type here, and the message ends with the fix, Did you mean 'typeof createCartItem'? A string gets Type 'string' does not satisfy the constraint '(...args: any) => any'. (...args: any) => any is how TypeScript writes "any function", and ReturnType only takes those. In both error cases, the query shows any: TypeScript gave up on the type.
These two helpers are most useful when you wrap a function: you write a new function that does something extra, then calls the original. Here, a shop logs every coupon a shopper tries:
function applyCoupon(total: number, code: string) { return code === "SAVE5" ? total - 5 : total;} function trackCoupon( ...args: Parameters<typeof applyCoupon>): ReturnType<typeof applyCoupon> { console.log("Coupon tried:", args[1]); return applyCoupon(...args);} console.log(trackCoupon(20, "SAVE5"));trackCoupon(20);Lines 1 to 3 apply a coupon: SAVE5 takes 5 off the total. Lines 5 to 10 wrap it. Line 6 collects every argument into the list args, typed Parameters<typeof applyCoupon>, so args is [total: number, code: string]. Line 7 says trackCoupon returns what applyCoupon returns, a number. Line 8 logs the code, and line 9 spreads args into applyCoupon.
Line 12 prints Coupon tried: SAVE5 and then 15. Line 13 forgets the code, and TypeScript marks it on purpose: Expected 2 arguments, but got 1, the same check a call to applyCoupon gets. It still runs and prints Coupon tried: undefined. If applyCoupon later takes a third argument, trackCoupon follows without an edit.
Unwrapping promises with Awaited
An async function always returns a promise: a value that arrives later (see Promises and async and await). So ReturnType of an async function is a promise type, not the data inside it. Press the buttons to name a loaded cart three ways:
async function fetchCart() { return { items: ["Tea", "Cake"], total: 7 };} type Cart = ReturnType<typeof fetchCart>;// ^? function cartSummary(cart: Cart) { return `${cart.items.length} items, $${cart.total}`;} fetchCart().then((cart) => console.log(cartSummary(cart)));2 items, $73 errors: lines 9 and 12fetchCart is async, so it returns a promise, and so does ReturnType: the answer after ^? starts with Promise<. Line 9 reads cart.items from a promise: Property 'items' does not exist on type 'Promise<. Line 12 passes the real cart where a promise is expected.
With ReturnType only, the query on line 6 shows Promise<{ items: string[]; total: number; }>. Line 9 gets Property 'items' does not exist on type 'Promise<…>' (and the same for total): a promise has no items, only the cart inside it does. Line 12 gets an error too, because .then hands cartSummary the real cart where it expects a promise.
Awaited of ReturnType unwraps the promise, and every error is gone. Awaited of the function unwraps nothing: typeof fetchCart is the function, not a promise, so Awaited gives it back as it is, and lines 9 and 12 get errors again. In every case the code prints 2 items, $7, because the types are removed before it runs.
Awaited follows the same rules as await. Like UnwrapAll from the infer lesson, it keeps unwrapping, and it leaves other types alone:
type Nested = Awaited<Promise<Promise<number>>>;// ^?type Plain = Awaited<string>;// ^?type Mixed = Awaited<boolean | Promise<number>>;// ^?Line 1 is a promise of a promise of a number. The query on line 2 shows number: Awaited unwraps until no promise is left. Line 3 gives it string, which isn't a promise, so the query on line 4 shows string. Line 5 is a union, and Awaited works on each member: boolean stays, and Promise<number> becomes number. The query on line 6 shows number | boolean.
Promise<Promise<number>> is a promise inside a promise. Awaited opens both and gives the number inside.A friend sends a gift in a box, packed inside a bigger box. You open the big box, then the small one, and stop when you reach the gift. If they hand you the gift with no box at all, there's nothing to open.
- In real life: A parcel with another parcel inside
- In TypeScript:
Promise<Promise<number>> - In real life: You open boxes until you reach the gift
- In TypeScript:
Awaitedunwraps until it reaches a type that isn't a promise - In real life: The gift itself
- In TypeScript:
number - In real life: A gift handed to you without a box
- In TypeScript:
Awaited<string>is juststring
For a variable, you rarely write Awaited yourself, because await already gives the unwrapped type:
async function fetchCart() { return { items: ["Tea", "Cake"], total: 7 };} async function showCart() { const cart = await fetchCart(); // ^? console.log(`${cart.items.length} items`);}showCart();Line 6 waits for fetchCart's promise. The query on line 7 shows cart is the cart itself, { items: string[]; total: number; }. Line 8 prints 2 items. You need Awaited when you want the name of that type without awaiting: for a function's parameter, like cartSummary(cart: Cart) in the playground, or for a variable you fill in later.
Shipping quotes on a checkout page
A checkout page asks a server what shipping costs. The function that asks is written once, and the page names its types from it:
type Speed = "standard" | "express" | "drone"; async function quoteShipping(speed: Speed, weightKg: number) { const pricePerKg = { standard: 3, express: 8, drone: 15 }; return { speed, price: pricePerKg[speed] * weightKg, days: speed === "standard" ? 4 : 2 };} type Quote = Awaited<ReturnType<typeof quoteShipping>>;// ^?type OfferedSpeed = Exclude<Parameters<typeof quoteShipping>[0], "drone">;// ^? function showQuote(quote: Quote) { console.log(`${quote.speed}: $${quote.price} in ${quote.days} days`);} const offered: OfferedSpeed[] = ["standard", "express"];for (const speed of offered) { quoteShipping(speed, 2).then(showQuote);}Line 1 lists three delivery speeds. Lines 3 to 6 ask for a quote: line 4 has a price per kilogram for each speed, and line 5 returns the speed, the price for the weight, and the days it takes. It's async, as a real request to a server would be.
Line 8 names the quote with Awaited of ReturnType, and the query on line 9 shows its three properties. Line 10 takes the first parameter's type, Speed, and excludes "drone", because this shop doesn't deliver by drone yet. The query on line 11 shows "standard" | "express".
Lines 13 to 15 print a quote. Line 17 lists the speeds the page offers; "drone" there would be an error. Lines 18 to 20 ask for a 2-kilogram quote at each speed. The code prints standard: $6 in 4 days and then express: $16 in 2 days. No type was written twice: add a speed or a property to quoteShipping, and Quote and OfferedSpeed follow.
The table below sums up the six helpers. Read each row as: the helper, what you give it, and what you get back.
| The helper | What you give it | What you get back |
|---|---|---|
Exclude<Union, Remove> | A union, and the members to take out | The union without the members that fit Remove |
Extract<Union, Keep> | A union, and the members to keep | Only the members that fit Keep |
NonNullable<T> | Any type | T without null and undefined |
ReturnType<typeof f> | A function's type | The type of what the function returns |
Parameters<typeof f> | A function's type | A tuple of what the function takes |
Awaited<T> | A promise type, or any type | The value inside the promise, unwrapped all the way |
type Item = Exclude<"tea" | "cake", "cake">; const item: Item = "tea";type Item = Extract<"tea" | "cake", "cake">; const item: Item = "tea";type Result = ReturnType<string>;function total() { return 7; } type Total = ReturnType<total>;function total() { return 7; } const sum: ReturnType<typeof total> = 9;const coupon: NonNullable<string | null> = null;type Drink = Exclude<"tea" | "cake", "coffee">;const total: Awaited<Promise<number>> = 7;
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
Exclude and Omit both take something out, so they're easy to swap. Omit (from the Partial, Pick, Omit & friends lesson) takes properties out of an object type:
type Order = { id: number; total: number; coupon: string | null }; type WithoutCoupon = Omit<Order, "coupon">;// ^?type NothingRemoved = Exclude<Order, "coupon">;// ^?Line 1 describes an order. Line 3 uses Omit to take out the coupon property, and the query on line 4 shows the id and total that are left. Line 5 tries Exclude instead. Order isn't a union of names; it's one object type, and it doesn't fit "coupon", so nothing is taken out. The query on line 6 shows all three properties, and no error warns you.
The table below compares them. Read each row as: what you write, what it works on, and what you get.
| When you write | What it works on | What you get |
|---|---|---|
Exclude<OrderStatus, "cancelled"> | A union: the members of a type like "paid" | "cancelled". | The union without "cancelled". |
Omit<Order, "coupon"> | An object type: its properties. | The object type without its coupon property. |
Exclude<Order, "coupon"> | An object type, which is one member, not a union of keys. | Order unchanged, with no error. Nothing in it fits "coupon". |
NonNullable only works on the type you give it, not on the properties inside an object:
type Profile = { name: string; nickname: string | null }; type Loaded = NonNullable<Profile | null>;// ^?type Nickname = Loaded["nickname"];// ^?Line 3 takes the null out of Profile | null, and the query on line 4 shows a profile. Line 5 reads its nickname property's type, and the query on line 6 still shows string | null.
- "
Excludetells me when I misspell a member." No.Exclude<OrderStatus, "canceled">takes out nothing and reports nothing. Check the result with^?or by hovering. - "
ReturnType<createCartItem>works." No. A function's name is a value. In a type, writetypeof createCartItem. - "
ReturnTypeof an async function is its data." No. It's a promise of the data. Wrap it:Awaited<ReturnType<typeof fetchCart>>. - "
NonNullableremovesnulleverywhere inside a type." No. It removesnullandundefinedfrom the type itself, not from its properties. - "
ExcludeandOmitdo the same job." No.Excludefilters a union.Omitremoves properties from an object type. - "
Awaitedwaits for the promise." No. Like every type, it's removed before the code runs. It only names the type the promise finishes with;awaitdoes the waiting.
Practice exercises
Each exercise is TypeScript. To change the starter code, press Edit & run: the errors and ^? answers update as you type, and the console shows what it prints. Try a guess first, then use the hints.
What is ShopPayment? Guess first. Then point at it, or press Edit & run and add a // ^? line under it.
type Payment = "card" | "cash" | "voucher" | "crypto";type ShopPayment = Exclude<Payment, "voucher" | "crypto">;type Payment = "card" | "cash" | "voucher" | "crypto";type ShopPayment = Exclude<Payment, "voucher" | "crypto">;// ^?"voucher" and "crypto" fit the second type, so Exclude takes them out. "card" and "cash" are left, and the query on line 3 shows type ShopPayment = "card" | "cash".
Which line will TypeScript mark? Then fix it.
function loadUser(id: number) {
return { id, name: "Asha" };
}
type User = ReturnType<loadUser>;function loadUser(id: number) { return { id, name: "Asha" };} type User = ReturnType<typeof loadUser>;// ^?Line 5 gives ReturnType the function itself, a value, so TypeScript says 'loadUser' refers to a value, but is being used as a type here. With typeof loadUser, the query shows type User = { id: number; name: string; }.
logRemove accepts every event, so line 12 logs Removed Tea for an add event. Change line 5 so it only accepts remove events. Which line does TypeScript mark then?
type CartEvent = | { type: "add"; name: string; price: number } | { type: "remove"; name: string }; type RemoveEvent = CartEvent; function logRemove(event: RemoveEvent) { console.log(`Removed ${event.name}`);} logRemove({ type: "remove", name: "Cake" });logRemove({ type: "add", name: "Tea", price: 3 });type CartEvent = | { type: "add"; name: string; price: number } | { type: "remove"; name: string }; type RemoveEvent = Extract<CartEvent, { type: "remove" }>; function logRemove(event: RemoveEvent) { console.log(`Removed ${event.name}`);} logRemove({ type: "remove", name: "Cake" });logRemove({ type: "add", name: "Tea", price: 3 });Extract<CartEvent, { type: "remove" }> keeps only the remove event. Line 12 passes an add event, so TypeScript marks it: Type '"add"' is not assignable to type '"remove"'.
TypeScript marks line 4: the nickname might be null. Change only the parameter's type on line 3, then fix the call. What does the fixed code print?
type Profile = { name: string; nickname: string | null }; function greet(nickname: Profile["nickname"]) { console.log(`Hi, ${nickname.toUpperCase()}!`);} const asha: Profile = { name: "Asha", nickname: "Ash" };greet(asha.nickname);type Profile = { name: string; nickname: string | null }; function greet(nickname: NonNullable<Profile["nickname"]>) { console.log(`Hi, ${nickname.toUpperCase()}!`);} const asha: Profile = { name: "Asha", nickname: "Ash" };if (asha.nickname) greet(asha.nickname);With NonNullable<Profile["nickname"]>, greet only takes text, so line 4 is fine. The call must then check for null first, and inside the if the nickname is text. It prints Hi, ASH!.
Product on line 5 should be one product, but it's a promise of a list. Fix line 5 so the errors go away. What does the code print?
async function fetchProducts(category: "tea" | "cake") { return [{ name: "Green tea", price: 3, category }];} type Product = ReturnType<typeof fetchProducts>; function label(product: Product) { return `${product.name}: $${product.price}`;} fetchProducts("tea").then((products) => console.log(label(products[0])));async function fetchProducts(category: "tea" | "cake") { return [{ name: "Green tea", price: 3, category }];} type Product = Awaited<ReturnType<typeof fetchProducts>>[number];// ^? function label(product: Product) { return `${product.name}: $${product.price}`;} fetchProducts("tea").then((products) => console.log(label(products[0])));Awaited unwraps the promise to a list of products, and [number] reads the type of one item, the way [0] read one parameter. The query shows the product's three properties, and the code prints Green tea: $3.
withLog uses any, so nothing is checked, and TypeScript marks line 7: A spread argument must either have a tuple type or be passed to a rest parameter. Replace both anys with types read from applyCoupon. What is the last thing the code prints?
function applyCoupon(total: number, code: string) { return code === "SAVE5" ? total - 5 : total;} function withLog(...args: any[]): any { console.log("applyCoupon", args.join(", ")); return applyCoupon(...args);} const total = withLog(20, "SAVE5");console.log(total);function applyCoupon(total: number, code: string) { return code === "SAVE5" ? total - 5 : total;} function withLog( ...args: Parameters<typeof applyCoupon>): ReturnType<typeof applyCoupon> { console.log("applyCoupon", args.join(", ")); return applyCoupon(...args);} const total = withLog(20, "SAVE5");// ^?console.log(total);Parameters<typeof applyCoupon> makes args the tuple [total: number, code: string], so spreading it into applyCoupon is allowed. ReturnType makes total a number instead of any. It prints applyCoupon 20, SAVE5, then 15.
Check your understanding
For each question, work out the helper's result one step at a time, from the inside out. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What is
OnSalein this menu code?Read the code, then predicttype Menu = "tea" | "cake" | "pie"; type OnSale = Extract<Menu, "cake" | "coffee">;Choose an answer to see the explanation.
Question 2 of 7What is
Loadedfor this async function?Read the code, then predictasync function loadCart() { return { total: 7 }; } type Loaded = ReturnType<typeof loadCart>;Choose an answer to see the explanation.
Question 3 of 7What does TypeScript say about this misspelled Exclude, and what is
Open?Read the code, then predicttype Status = "paid" | "shipped" | "cancelled"; type Open = Exclude<Status, "canceled">;Choose an answer to see the explanation.
Question 4 of 7Which line does TypeScript flag in this coupon code?
Read the code, then predictfunction applyCoupon(total: number, code: string) { return code === "SAVE5" ? total - 5 : total; } type CouponArgs = Parameters<applyCoupon>;Choose an answer to see the explanation.
Question 5 of 7In
Loaded, what is the type ofnickname?Read the code, then predicttype Profile = { name: string; nickname: string | null }; type Loaded = NonNullable<Profile | null>;Choose an answer to see the explanation.
Question 6 of 7What is
Resultafter Awaited unwraps this union?Read the code, then predicttype Result = Awaited<Promise<Promise<number>> | string>;Choose an answer to see the explanation.
Question 7 of 7Which helper gives the types of a function's arguments, as a list?
Choose an answer to see the explanation.
Key takeaways
Exclude<Union, Remove>takes out the members that fitRemove.Extract<Union, Keep>keeps only the members that fitKeep.- Neither one warns you when the second type matches nothing, so check the result.
NonNullable<T>removesnullandundefinedfromTitself, not from its properties.ReturnTypeandParametersread a function's type, which you get withtypeof:ReturnType<typeof createCartItem>.Awaitedunwraps promises all the way. For an async function, writeAwaited<ReturnType<typeof fetchCart>>.- Types made this way follow the type or function they come from, so you never keep two copies in step by hand.
Remember the one-liner.
Utility types build new types from the ones you have: filter a union, drop null, read a function, or unwrap a promise.
Coming next: Writing your own utility types. It rebuilds Partial and Pick from scratch, then writes helpers of your own, like a DeepPartial for settings with objects inside.