TypeScript's type system is incredibly powerful. Beyond basic type annotations, it offers a rich set of features that allow you to express complex constraints and relationships in your code.
Conditional Types
Conditional types allow you to create types that depend on other types:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Mapped Types
Mapped types let you create new types by transforming existing ones:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
Template Literal Types
Template literal types enable string manipulation at the type level:
type EventName = `on${Capitalize<string>}`;
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/${string}`;
type Route = `${HTTPMethod} ${Endpoint}`;
Practical Patterns
The Builder Pattern
class QueryBuilder<T extends Record<string, unknown>> {
private conditions: string[] = [];
where<K extends keyof T>(key: K, value: T[K]): this {
this.conditions.push(`${String(key)} = ${value}`);
return this;
}
build(): string {
return `SELECT * WHERE ${this.conditions.join(' AND ')}`;
}
}
Discriminated Unions
type Result<T, E> =
| { success: true; data: T }
| { success: false; error: E };
function handleResult(result: Result<string, Error>) {
if (result.success) {
console.log(result.data); // TypeScript knows data exists
} else {
console.error(result.error); // TypeScript knows error exists
}
}
Key Takeaways
- Start simple - Don't over-engineer your types
- Use generics - They make your code reusable and type-safe
- Leverage inference - TypeScript can often figure out types automatically
- Use utility types -
Partial,Required,Pick,Omitare your friends
Good types are documentation that never goes out of date.