Execute Program

Advanced TypeScript: Conditional Types

Welcome to the Conditional Types lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • In a ternary conditional like someBool ? trueValue : falseValue, we get trueValue when someBool is true. Otherwise, we get falseValue.

  • >
    const x = 5;
    x > 0 ? x + 1 : x;
    Result:
    6Pass Icon
  • >
    const x = -2;
    x > 0 ? x + 1 : x;
    Result:
    -2Pass Icon
  • TypeScript has a feature called "conditional types" that uses the same ternary syntax. A conditional type looks like SomeCondition ? T1 : T2. The ternaries in our code examples above operated on values, but conditional types operate on types at compile time. We build one type or another depending on what other types already exist.

  • We'll start with a small conditional type example: a WrapStringInArray<T> type. When we apply this type to a string, it gives us the type Array<string>. When we apply it to any other type, it leaves the type alone.

  • (Note that one of the examples below causes a type error!)

  • >
    type WrapStringInArray<T> = T extends string ? Array<string> : T;
    const s: WrapStringInArray<string> = ['hello'];
    s;
    Result:
    ['hello']Pass Icon
  • >
    type WrapStringInArray<T> = T extends string ? Array<string> : T;
    const s: WrapStringInArray<string> = 'hello';
    s;
    Result:
    type error: Type 'string' is not assignable to type 'string[]'.Pass Icon
  • >
    type WrapStringInArray<T> = T extends string ? Array<string> : T;
    const n: WrapStringInArray<number> = 1;
    n;
    Result:
    1Pass Icon
  • The condition here is T extends string. We've already seen the extends keyword in other contexts. It's used in inheritance: class Cat extends Pet. It's also used in generic constraints: function filterBelowAge<T extends {age: number}> { ... }.

  • "Extending a type" is a core idea in TypeScript, which is why this keyword shows up in so many places. We can think of "extends" as "is a kind of". For example, 'Ms. Fluff' extends string is true because literal string types are a kind of string. But number extends string is false because numbers aren't a kind of string.

  • Type extension follows common-sense rules in most cases. For example, every type extends itself: string extends string, Array<number> extends Array<number>, etc. This matches the way that equality works in JavaScript and other programming languages, where 5 == 5, 'Amir' == 'Amir', etc. (There's technically one exception to that in most programming languages: NaN != NaN. But TypeScript's extends has no such exceptions.)

  • When we apply WrapStringInArray<T> to a type, the compiler uses these type extension rules to check T extends string ? Array<string> : T. If T extends string is true, we get Array<string>. If it's false, we get T.

  • In conditional types, the conditional always looks like SomeType extends SomeOtherType. The types on either side of extends can be simple or complex, but there's always an extends between them.

  • Conditional types are often combined with mapped types. In the next few examples, we extend our WrapStringInArray type to work on each property of an object type. All of the string types become Array<string>, but non-string types are left alone.

  • >
    type WrapStringsInArrays<T> = {
    [K in keyof T]: T[K] extends string ? Array<string> : T[K]
    };

    type User = {
    name: string
    email: string
    age: number
    };
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const amir: WrapStringsInArrays<User> = {
    name: ['Amir'],
    email: ['amir@example.com'],
    age: 36,
    };
    amir.name;
    Result:
    ['Amir']Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const amir: WrapStringsInArrays<User> = {
    name: 'Amir',
    email: ['amir@example.com'],
    age: 36,
    };
    amir.name;
    Result:
    type error: Type 'string' is not assignable to type 'string[]'.Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const amir: WrapStringsInArrays<User> = {
    name: ['Amir'],
    email: ['amir@example.com'],
    age: [36],
    };
    amir.name;
    Result:
    type error: Type 'number[]' is not assignable to type 'number'.Pass Icon
  • Those two error messages show that our type did what we wanted: only string properties are wrapped in arrays. The type WrapStringsInArrays<User> is equivalent to {name: Array<string>, email: Array<string>, age: number}.

  • In the next code example, you'll write a mapped type, using a conditional type for the properties. Its structure is very similar to the WrapStringsInArrays type above; only the T1 ? T2 : T3 conditional part is different.

  • Here's a code problem:

    The code below defines a ReplaceNumberPropertiesWithNull type. It should transform an object type into another object type. Like its name says, it should replace all number properties with the null type. Other properties should be left alone.

    type ReplaceNumberPropertiesWithNull<T> = {
    [K in keyof T]: T[K] extends number ? null : T[K]
    };
    type User = {
    name: string
    age: number
    };

    /* We make two different Amir variables to make sure that the type has
    * exactly the property types we expect. */
    const amir1: ReplaceNumberPropertiesWithNull<User> = {
    name: 'Amir',
    age: null,
    };
    const amir2: {name: string, age: null} = amir1;
    amir2;
    Goal:
    {name: 'Amir', age: null}
    Yours:
    {name: 'Amir', age: null}Pass Icon
  • Let's briefly examine what it means for type conditions to be "true" or "false". When learning about conditional types, it would be nice to experiment with code like console.log('hello' extends string). The type condition is true in that case, but that code doesn't work. It's a syntax error!

  • >
    console.log('hello' extends string);
    Result:
  • The problem is that we're trying to mix type-level code and value-level code. Type expressions like 'hello' extends string only participate in the type system. They don't have runtime values at all because the TypeScript compiler discards all types before generating JavaScript code. If the types don't exist at runtime, and 'hello' extends string is part of the types, then it doesn't make sense to try to log that type expression. There's nothing to log.

  • A final note on applications of conditional types. They can seem very esoteric, and to some extent they are. Regular application code rarely needs conditional types. You're unlikely to use them while writing most React or Vue components, or while writing most everyday API endpoint handlers in a backend server.

  • The examples in this lesson were also contrived to keep them simple. If we really wanted a variant of the User type with arrays instead of regular strings, we could've just written that type out explicitly.

  • However, conditional and mapped types are critical in code that needs to be highly generic, like library and framework code. For example, many of TypeScript's utility types are implemented using conditional types, often in combination with mapped types and other advanced features. Without conditional types, types like ReturnType would simply be impossible. Future lessons will show how some of TypeScript's utility types are defined using conditional types.