Conditional Types
Conditional types allow you to conditionally choose a type based on a condition. The syntax for a conditional type is T extends U ? X : Y
, where T
is the type being tested, U
is the condition being tested against, X
is the type returned if the condition is true, and Y
is the type returned if the condition is false.
For example, let’s say you have a function that takes a type T
and returns a new type based on whether T
is an array or not:
type IsArray<T> = T extends Array<any> ? true : false;
type MyType = IsArray<number>; // MyType is false
type MyOtherType = IsArray<number[]>; // MyOtherType is true
In this example, IsArray
is a conditional type that tests whether T
extends Array<any>
. If it does, the type true
is returned; if it doesn’t, the type false
is returned. The types MyType
and MyOtherType
demonstrate how the IsArray
type works.
Mapped Types
Mapped types allow you to create new types by mapping over an existing type. The syntax for a mapped type is { [P in K]: T }
, where P
is the property key, K
is a union of property keys, and T
is the type of the property.
For example, let’s say you have an interface with a bunch of optional properties, and you want to create a new interface that makes all those properties required:
interface MyInterface {
foo?: number;
bar?: string;
}
type RequiredMyInterface = { [P in keyof MyInterface]-?: MyInterface[P] };
In this example, RequiredMyInterface
is a mapped type that maps over the keys of MyInterface
. The -?
makes each property required, and the type MyInterface[P]
sets the type of the property.
Type Aliases
Type aliases allow you to create a new name for an existing type. The syntax for a type alias is type NewType = OldType
.
For example, let’s say you have a function that takes a callback and returns a new function that takes the same arguments as the original function, but with an additional callback
parameter:
type Callback<T> = (arg: T) => void;
type NewCallback<T> = (arg: T, callback: Callback<T>) => void;
function addCallback<T>(fn: (arg: T) => void): NewCallback<T> {
return (arg, callback) => {
fn(arg);
callback(arg);
};
}
In this example, Callback
and NewCallback
are type aliases that define the types of the callback functions used by addCallback
.
Text Diagram
Here’s a text diagram to help you understand how conditional types and mapped types work:
MyType:
T extends Array<any> ? true : false
MyOtherType:
T extends Array<any> ? true : false
RequiredMyInterface:
{ [P in keyof MyInterface]-?: MyInterface[P] }
In this diagram, MyType
and MyOtherType
are examples of conditional types, while RequiredMyInterface
is an example of a mapped type.
Code Examples
Here are some additional code examples to help you understand these advanced TypeScript features:
Conditional Types
type IfEquals =
(() => T extends X ? 1 : 2) extends
(() => T extends Y ? 1 : 2) ? A : B;
type MyType = IfEquals; // MyType is false
type MyOtherType = IfEquals; // MyOtherType is false
Mapped Types
interface MyObject {
foo: number;
bar: string;
}
type ReadonlyMyObject = { readonly [P in keyof MyObject]: MyObject[P] };
const obj: ReadonlyMyObject = { foo: 1, bar: "hello" };
obj.foo = 2; // Error: Cannot assign to 'foo' because it is a read-only property
In this example, we define an interface MyObject
with two properties, foo
and bar
. We then create a mapped type ReadonlyMyObject
that maps over the keys of MyObject
and sets each property to readonly
. Finally, we create an object obj
of type ReadonlyMyObject
, which prevents us from modifying any of its properties.
Type Aliases
type User = {
id: number;
name: string;
};
type UserID = User["id"];
const user: User = { id: 1, name: "John" };
const userID: UserID = user.id; // userID is of type number
In this example, we define a type User
with two properties, id
and name
. We then create a type alias UserID
that refers to the id
property of User
. Finally, we create an object user
of type User
and extract its id
property into a variable userID
of type UserID
.