Advanced TypeScript Types Cheat Sheet (with Examples)
TypeScript‘s advanced type system is one of its most powerful features, enabling developers to write cleaner, more maintainable code. However, with so many advanced types available, it can be challenging to remember the syntax and use cases for each one.
In this cheat sheet, we‘ll cover all the essential advanced TypeScript types with clear explanations and practical examples. Whether you need a quick reference or want to deepen your understanding of TypeScript‘s capabilities, this guide has you covered.
Let‘s dive in!
Intersection Types
Intersection types allow you to combine multiple types into one, giving you the flexibility to define entities that share characteristics from different sources. Think of it like a Venn diagram, where the intersection type contains only the common properties.
Here‘s the basic syntax:
type Foo = {
a: string;
b: number;
}
type Bar = {
b: number;
c: boolean;
}
type FooBar = Foo & Bar;
let variable: FooBar = {
a: ‘hello‘,
b: 42,
c: true
}
In this example, FooBar
is an intersection of the Foo
and Bar
types. Any variable of type FooBar
must have all the properties from both constituent types.
Intersection types are useful when you need to merge configuration options or share fields between different entities. For instance, say you have a User
type and an Account
type:
type User = {
id: string;
email: string;
}
type Account = {
id: string;
balance: number;
}
type UserAccount = User & Account;
Now the UserAccount
type has all properties of both User
and Account
, enabling you to define variables that are both a user and an account.
Union Types
Where intersection types require shared properties, union types allow a value to be one of several types. They are defined using the vertical bar (|
) character.
type Status = ‘active‘ | ‘inactive‘;
function setStatus(status: Status) {
// ...
}
setStatus(‘active‘); // valid
setStatus(‘pending‘); // error
Here, the Status
type can only be ‘active‘
or ‘inactive‘
, enforcing valid values at compile time.
Union types are handy for defining possible string literal values, like in the example above, but they also work with other types:
type Foo = string | number;
function bar(foo: Foo) {
// ...
}
bar(‘hello‘); // valid
bar(42); // valid
bar(true); // error
In this case, bar
accepts either a string
or number
parameter thanks to the Foo
union type.
Generic Types
Generics allow you to write reusable code that works with multiple types rather than a single one. They are essentially a way to parameterize types, similar to how functions parameterize values.
The syntax for generics uses angle brackets (<>
) to enclose the type parameter:
function identity<T>(arg: T): T {
return arg;
}
let foo = identity<string>(‘hello‘);
let bar = identity<number>(42);
The identity
function takes a type parameter T
and an argument of that same type, then returns a value of that type. When calling identity
, you specify the concrete type in angle brackets, locking in T
for that invocation.
Generics are invaluable for writing utility functions that deal with collections or promise results:
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
type User = {
name: string;
email: string;
}
fetchData<User>(‘/api/users/1‘)
.then(user => {
console.log(user.name);
});
By making fetchData
generic, we can specify the expected return type when calling it, giving us type safety and autocompletion without needing to manually assert the result type.
Utility Types
TypeScript provides several utility types that facilitate common type transformations. These types are globally available and typically start with a capital letter. Let‘s examine a few of the most frequently used ones.
Partial
Partial<T>
transforms all properties of T
into optional properties, meaning they can be omitted.
type User = {
name: string;
email: string;
}
type PartialUser = Partial<User>;
// Valid
const partialUser: PartialUser = {
name: ‘John‘
};
This is useful when you need to represent subsets of data or define configuration objects with default values.
Required
Required<T>
does the opposite of Partial
, making all properties of T
required.
type User = {
name?: string;
email?: string;
}
type RequiredUser = Required<User>;
// Error: email is missing
const reqUser: RequiredUser = {
name: ‘John‘
};
Readonly
Readonly<T>
makes all properties of T
read-only, preventing reassignment.
type User = {
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = {
name: ‘John‘,
email: ‘[email protected]‘
};
user.name = ‘Johnny‘; // Error: name is read-only
This protects objects from unintended mutation and can serve as a performance optimization hint to JavaScript engines.
Pick
Pick<T, K>
constructs a new type by picking a set of properties K
from T
.
type User = {
name: string;
email: string;
age: number;
}
type NameAndEmail = Pick<User, ‘name‘ | ‘email‘>;
const nameAndEmail: NameAndEmail = {
name: ‘John‘,
email: ‘[email protected]‘
};
This is handy for slicing off parts of existing types when you don‘t need all properties.
Omit
Omit<T, K>
is the complement to Pick
, creating a new type with properties K
removed from T
.
type User = {
name: string;
email: string;
age: number;
}
type UserWithoutAge = Omit<User, ‘age‘>;
const userWithoutAge: UserWithoutAge = {
name: ‘John‘,
email: ‘[email protected]‘
};
This lets you exclude irrelevant or sensitive properties from types.
Mapped Types
Mapped types build on the syntax for index signatures, which are used to declare the types of properties that have not been declared ahead of time:
type Foo = {
[key: string]: number;
}
Here, Foo
has a string index signature, meaning Foo
is a type with any number of properties of type number
indexed by a string
.
Mapped types allow you to create new types based on old ones by transforming each property in the original type:
type Stringify<T> = {
[K in keyof T]: string;
}
type User = {
name: string;
age: number;
}
type StringifiedUser = Stringify<User>;
// same as
// type StringifiedUser = {
// name: string;
// age: string;
// }
The mapped type Stringify<T>
takes each property of T
and transforms its type to string
. You can also leverage mapped types to make properties optional or read-only:
type Partial<T> = {
[K in keyof T]?: T[K];
}
type Readonly<T> = {
readonly [K in keyof T]: T[K];
}
These examples implement the Partial
and Readonly
utility types discussed earlier. Mapped types open up a world of possibilities for generating new types based on existing ones.
Type Guards
Type guards are expressions that perform a runtime check to narrow down, or constrain, the type of a variable within a conditional block. There are several ways to define a type guard in TypeScript.
typeof
The typeof
type guard checks the type of a variable against a string literal:
function foo(bar: string | number) {
if (typeof bar === ‘string‘) {
// bar is a string here
return bar.toUpperCase();
} else {
// bar is a number here
return bar.toFixed(2);
}
}
Inside the if
block, TypeScript knows bar
is a string
so we can call string
methods like toUpperCase
. In the else
block, bar
is narrowed to a number
.
instanceof
The instanceof
type guard checks whether a value is an instance of a particular constructor function:
class Foo {
foo = ‘foo‘;
}
class Bar {
bar = ‘bar‘;
}
function baz(arg: Foo | Bar) {
if (arg instanceof Foo) {
console.log(arg.foo); // OK
console.log(arg.bar); // Error: Property ‘bar‘ does not exist on type ‘Foo‘
} else {
console.log(arg.bar); // OK
console.log(arg.foo); // Error: Property ‘foo‘ does not exist on type ‘Bar‘
}
}
The instanceof
guard lets TypeScript narrow arg
to either the Foo
or Bar
class inside the conditional blocks.
in
The in
type guard checks for the existence of a property on an object:
interface Foo {
foo: string;
}
interface Bar {
bar: string;
}
function baz(arg: Foo | Bar) {
if (‘foo‘ in arg) {
console.log(arg.foo); // OK
console.log(arg.bar); // Error
} else {
console.log(arg.bar); // OK
console.log(arg.foo); // Error
}
}
Similar to instanceof
, the in
guard narrows arg
to Foo
or Bar
based on whether it has a ‘foo‘
property.
Type guards enable powerful control flow analysis that lets you write expressive conditional logic operating on TypeScript‘s static types.
Conditional Types
Conditional types introduce the ability for a type to make a decision based on another type. They take the form T extends U ? X : Y
, meaning "if T
is assignable to U
, return type X
, otherwise return type Y
".
Here‘s a simple example:
type IsNumber<T> = T extends number ? ‘yes‘ : ‘no‘;
type Foo = IsNumber<42>; // ‘yes‘
type Bar = IsNumber<‘hello‘>; // ‘no‘
The IsNumber
conditional type checks if its type parameter T
is assignable to number
. If so, it returns the type ‘yes‘
, else it returns ‘no‘
.
Conditional types become especially powerful when combined with generics and mapped types:
type ElementType<T> = T extends (infer U)[] ? U : T;
type Foo = ElementType<number[]>; // number
type Bar = ElementType<string>; // string
Here, ElementType<T>
checks if T
is an array type using an infer
declaration to extract the element type U
. If T
is an array, it returns U
, else it just returns T
.
Conditional types give you a way to express complex type relationships and transformations that can adapt to the input type. They are a key building block for many advanced TypeScript techniques.
Conclusion
TypeScript‘s rich type system provides an array of tools for modeling your application domain and catching errors at compile time. By leveraging advanced types like intersections, unions, generics, and mapped types, you can create expressive and reusable type definitions.
This cheat sheet has covered the key concepts you need to know, along with practical examples and use cases. Bookmark it as a handy reference, and don‘t be afraid to experiment with combining different types to capture your unique constraints.
As you dive deeper into TypeScript, keep exploring its type system and push the boundaries of what‘s possible. You may be surprised at just how much complexity you can model and how many errors you can catch before they ever make it to runtime. Happy typing!