Having Fun with TypeScript: Conditional Types
This blog post is part of the series Having Fun with TypeScript.
Introduction
Conditional types are an advanced feature in TypeScript that empower developers to express complex type relationships and transformations based on logical conditions. Think of them as the type system’s version of the ternary operator (condition ? resultIfTrue : resultIfFalse
). They allow you to create new types dynamically, enabling greater flexibility and type safety within your TypeScript code.
What are Conditional Types?
At their core, conditional types take the following form:
TypeA extends TypeB ? TrueType : FalseType
Let’s break down the components:
- TypeA: The type being checked.
- extends: The keyword signifying a type constraint.
- TypeB: The type against which
TypeA
is compared. - TrueType: The type returned if the condition (
TypeA
extendsTypeB
) evaluates to true. - FalseType: The type returned if the condition evaluates to false.
Why Use Conditional Types?
Type Inference with Precision: Conditional types allow TypeScript to infer and narrow down types more accurately within your codebase. This enhanced inference reduces the need for manual type annotations while making your code more self-documenting.
Reusable Type Logic: You can encapsulate reusable type transformation logic within conditional types, promoting cleaner code and minimizing repetition.
Flexible APIs: Conditional types let you design APIs that offer different return types or behaviors depending on the input types. This leads to more versatile functions and components.
Practical Examples
Filtering out null
and undefined
type NonNull<T> = T extends null | undefined ? never : T;
// OK: Type is 'string'
let nonNullValue1: NonNull<string | null> = "hello";
// Error: Type 'null' is not assignable to type 'string'.
let nonNullValue2: NonNull<string | null> = null;
Extracting Array Element Types
type Flatten<T> = T extends any[] ? T[number] : T;
type Flattened1 = Flatten<string[]>; // Type is 'string'
let flattened1 : Flattened1 = 'hello';
type Flattened2 = Flatten<number>; // Type is 'number'
let flattened2 : Flattened2 = 123;
Explanation:
- Conditional Type: This defines a “conditional type.” It checks a type
T
and provides different results based on whetherT
meets a certain condition. - Condition:
T extends any[]
This part checks if the provided typeT
is an array (or more accurately, if it can be assigned to theany[]
type). - True Branch:
T[number]
IfT
is an array, theFlatten
type becomesT[number]
. Arrays in TypeScript are indexed by numbers, so this is extracting the element type of the array. - False Branch:
T
IfT
is not an array, the type remains unchanged.
Modeling Options
interface User {
name: string;
age: number;
}
interface UserWithOptionalEmail {
name: string;
age: number;
email?: string;
}
type WithEmail<T> = T extends { email: string } ? T : T & UserWithOptionalEmail;
function processUser(user: WithEmail<User>) {
if (user.email) {
console.log("User email: ", user.email);
} else {
console.log("The user doesn't have an email");
}
}
const userWithEmail: WithEmail<User> = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
const userWithoutEmail: WithEmail<User> = {
name: "Bob",
age: 25
};
processUser(userWithEmail);
processUser(userWithoutEmail);
Advanced Concepts
The infer
Keyword
Introduce new type variables within conditional types to capture and infer types.
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function addNumbers(x: number, y: number): number {
return x + y;
}
function greetUser(name: string): string {
return "Hello, " + name + "!";
}
// Get the return types
type AddNumbersReturnType = GetReturnType<typeof addNumbers>; // type will be 'number'
let num : AddNumbersReturnType = 123;
type GreetUserReturnType = GetReturnType<typeof greetUser>; // type will be 'string'
let s : GreetUserReturnType = 'hello';
type NeverType = GetReturnType<string>; // type will be 'never'
// Error: Type 'null' is not assignable to type 'never'.
let n : NeverType = null;
Explanation:
The GetReturnType
type is a powerful TypeScript utility type that extracts the return type of a function. Here's how it works:
- Generic Type: It’s a generic type, meaning it takes another type (
T
) as an argument. - Conditional Type: It employs a conditional type (
T extends ... ? ... : ...
) to check if the input typeT
is a function. infer
Keyword: IfT
is a function, theinfer R
mechanism is used to capture the function's return type into a new type variableR
.- Return Value: Finally,
GetReturnType<T>
returns eitherR
(the inferred return type) ornever
ifT
is not a function.
Distributive Conditional Types
Distributive conditional types in TypeScript apply a conditional type check to each member of a union type individually. The result of the conditional type is then formed into a new union. In a nutshell, they “distribute” over union types.
Let’s consider this example:
type BoxedValue<T> = T extends any ? { value: T } : never;
// BoxedValue<number | string > = { value: string; } | { value: number; }
let value1: BoxedValue<number | string > = { value : 123 };
let value2: BoxedValue<number | string> = { value : 'hello' };
// Error: Type 'number' is not assignable to type '{ value: string; } | { value: number; }'
let value3: BoxedValue<number | string> = 456;
Here’s how it works:
BoxedValue<T>
: This conditional type takes a generic typeT
. IfT
is any type (any
), it returns an object{ value: T }
(wrapping the value in a box). Otherwise, it returnsnever
.BoxedValues
: We have a union typenumber | string
. Due to the distributive nature of the conditional type,BoxedValue
is applied to each member of the union:BoxedValue<number>
->{ value: number }
BoxedValue<string>
->{ value: string }
Conclusion
Conditional types are a powerful tool in your TypeScript arsenal. By understanding how to apply them, you can achieve:
- More expressive and type-safe code
- Enhanced type inference capabilities
- Increased flexibility and reusability within your codebase
While they may initially seem a bit complex, the benefits conditional types provide make them well worth the effort of learning.