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 gettrueValuewhensomeBoolis true. Otherwise, we getfalseValue.>
const x = 5;x > 0 ? x + 1 : x;Result:
6
>
const x = -2;x > 0 ? x + 1 : x;Result:
-2
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 astring, it gives us the typeArray<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']
>
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[]'.
>
type WrapStringInArray<T> = T extends string ? Array<string> : T;const n: WrapStringInArray<number> = 1;n;Result:
1
The condition here is
T extends string. We've already seen theextendskeyword 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 stringis true because literal string types are a kind of string. Butnumber extends stringis 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, where5 == 5,'Amir' == 'Amir', etc. (There's technically one exception to that in most programming languages:NaN != NaN. But TypeScript'sextendshas no such exceptions.)When we apply
WrapStringInArray<T>to a type, the compiler uses these type extension rules to checkT extends string ? Array<string> : T. IfT extends stringis true, we getArray<string>. If it's false, we getT.In conditional types, the conditional always looks like
SomeType extends SomeOtherType. The types on either side ofextendscan be simple or complex, but there's always anextendsbetween them.Conditional types are often combined with mapped types. In the next few examples, we extend our
WrapStringInArraytype to work on each property of an object type. All of thestringtypes becomeArray<string>, but non-stringtypes are left alone.>
type WrapStringsInArrays<T> = {[K in keyof T]: T[K] extends string ? Array<string> : T[K]};type User = {name: stringemail: stringage: 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']
- 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[]'.
- 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'.
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
WrapStringsInArraystype above; only theT1 ? T2 : T3conditional part is different.Here's a code problem:
The code below defines a
ReplaceNumberPropertiesWithNulltype. It should transform an object type into another object type. Like its name says, it should replace allnumberproperties with thenulltype. Other properties should be left alone.type ReplaceNumberPropertiesWithNull<T> = {[K in keyof T]: T[K] extends number ? null : T[K]};type User = {name: stringage: 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}
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 stringonly 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 stringis 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
Usertype 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
ReturnTypewould simply be impossible. Future lessons will show how some of TypeScript's utility types are defined using conditional types.