web-development typescript advanced-types mapped-types conditional-types generics utility-types typescript-generics typescript-advanced-types typescript-cheatsheet
TypeScript Generics and Advanced Types Cheatsheet: Master Complex Type Systems
---
Introduction
TypeScript is a powerful extension of JavaScript that adds static typing to your code, helping to catch errors early and improve overall code quality. One of the most powerful features of TypeScript is its support for generics and advanced types. These tools allow you to create more flexible, reusable, and type-safe code. In this cheatsheet, we’ll explore TypeScript’s generics and advanced types, helping you master complex type systems and take your TypeScript skills to the next level.
1. Understanding TypeScript Generics
Generics are a way to create components or functions that can work with any data type while still maintaining type safety. This is particularly useful for creating reusable code.
1.1 Basic Generics
Generics allow you to define a placeholder type that will be replaced with a specific type when the function or class is used.
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42); // T is replaced with number
const str = identity<string>('Hello'); // T is replaced with string
In this example, T
is a type variable that allows the identity
function to accept and return any type.
1.2 Generic Functions
Generics can be used in more complex functions that operate on arrays or other data structures.
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const firstNumber = getFirstElement([1, 2, 3]); // number
const firstString = getFirstElement(['a', 'b', 'c']); // string
This function works with any array type and returns the first element, maintaining the type of the elements.
1.3 Generic Classes
Generics can also be applied to classes, allowing for the creation of flexible and reusable data structures.
class Box<T> {
private contents: T;
constructor(value: T) {
this.contents = value;
}
getContents(): T {
return this.contents;
}
}
const numberBox = new Box<number>(123);
const stringBox = new Box<string>('Hello');
In this example, Box
can store and return any type, depending on what it is initialized with.
2. Advanced Generic Constraints
Generics can be constrained to ensure they meet certain criteria, which provides additional type safety.
2.1 Using extends
to Constrain Generics
You can use the extends
keyword to constrain a generic type to a subset of types that satisfy a particular condition.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person = { name: 'Alice', age: 25 };
const name = getProperty(person, 'name'); // Works
// const invalid = getProperty(person, 'invalidKey'); // Error: Argument of type '"invalidKey"' is not assignable to parameter of type '"name" | "age"'.
Here, K
is constrained to the keys of the object T
, ensuring that only valid property names can be used.
2.2 Default Generic Types
You can provide default types for generics, which will be used if no type is specified.
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const stringArray = createArray(3, 'x'); // string[]
const numberArray = createArray<number>(3, 42); // number[]
If no type is provided, T
defaults to string
.
3. Advanced Types
TypeScript provides several advanced types that allow you to describe complex type transformations and constraints.
3.1 Intersection Types
Intersection types combine multiple types into one. An object of an intersection type must satisfy all the combined types.
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
type EmployeePerson = Person & Employee;
const john: EmployeePerson = {
name: 'John Doe',
employeeId: 1234,
};
In this example, EmployeePerson
must have both name
and employeeId
properties.
3.2 Union Types
Union types allow a value to be one of several types.
function formatInput(input: string | number) {
if (typeof input === 'string') {
return input.toUpperCase();
}
return input.toFixed(2);
}
const formattedString = formatInput('hello'); // 'HELLO'
const formattedNumber = formatInput(3.14159); // '3.14'
Here, input
can be either a string
or a number
, and the function handles each case accordingly.
3.3 Mapped Types
Mapped types allow you to create new types by transforming existing ones. This is especially useful for applying the same transformation to each property in a type.
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface Todo {
title: string;
description: string;
}
const todo: Readonly<Todo> = {
title: 'Learn TypeScript',
description: 'Study generics and advanced types',
};
// todo.title = 'New Title'; // Error: Cannot assign to 'title' because it is a read-only property.
In this example, Readonly
makes every property in the Todo
interface read-only.
3.4 Conditional Types
Conditional types allow you to express types that depend on a condition.
type IsString<T> = T extends string ? 'Yes' : 'No';
type A = IsString<string>; // 'Yes'
type B = IsString<number>; // 'No'
This type checks whether a given type is a string and returns 'Yes'
or 'No'
accordingly.
4. Utility Types
TypeScript provides several built-in utility types that simplify common type transformations.
4.1 Partial
Partial<T>
makes all properties in T
optional.
interface User {
id: number;
name: string;
age: number;
}
const updateUser: Partial<User> = {
name: 'New Name', // id and age are optional
};
4.2 Pick
Pick<T, K>
creates a type by picking the set of properties K
from T
.
interface User {
id: number;
name: string;
age: number;
}
type UserName = Pick<User, 'name'>;
const userName: UserName = {
name: 'Alice',
};
4.3 Omit
Omit<T, K>
creates a type by omitting the set of properties K
from T
.
interface User {
id: number;
name: string;
age: number;
}
type UserWithoutAge = Omit<User, 'age'>;
const user: UserWithoutAge = {
id: 1,
name: 'Bob',
};
5. Best Practices for Using Generics and Advanced Types
- Use Generics for Reusability: Generics are great for creating reusable components and functions that work with different types.
- Constrain Generics When Necessary: Use
extends
to limit the types that a generic can accept, ensuring type safety.
- Leverage Advanced Types for Complex Structures: Use advanced types like intersection, union, and mapped types to handle complex data structures and transformations.
- Utilize Utility Types: TypeScript’s utility types can simplify type transformations, making your code cleaner and more maintainable.
Conclusion
TypeScript’s generics and advanced types provide powerful tools for building flexible, type-safe, and maintainable code. By mastering these features, you can write more robust TypeScript applications that can handle a variety of data structures and use cases. This cheatsheet serves as a quick reference to help you implement these concepts in your projects, making your TypeScript code more powerful and versatile.
Comments
Please log in to leave a comment.