Beyond the Basics: A Curated List of TypeScript Tricks for Better Coding
This is a list of TypeScript types and patterns that I’ve found useful in everyday coding. I’ve omitted the well-known ones, so if you’re looking for the basics, you might want to check out the TypeScript documentation first.
Prettify
When you union or intersect types, the resulting type can be hard to read. For example:
type User = { name: string }; type Image = { url: string; alt: string }; type UserOrImage = User | Image; // ^? UserOrImage = User | Image type UserWithProfilePicture = User & { profilePicture: Image }; // ^? type UserWithProfilePicture = User & { // profilePicture: Image // }
Wouldn’t it be nice if TypeScript would automatically expand the types for you?
With Prettify
, and its recursive counterpart DeepPrettify
, you can do just that.
type Prettify<T> = { [K in keyof T]: T[K]; } & {}; type DeepPrettify<T> = { [K in keyof T]: DeepPrettify<T[K]>; } & {};
Examples:
type UserOrImage = Prettify<User | Image>; // ^? { // name: string; // } | { // url: string; // alt: string; // } type UserWithProfilePicture = DeepPrettify<User & { profilePicture: Image }>; // ^? type UserWithProfilePicture = { // name: string; // profilePicture: { // url: string; // alt: string; // }; // }
ExactKeyOf
When you have an object, you can get the keys of that object through the keyof
operator. However, when you have Record<string, any>
, keyof
will return string | number | symbol
. Using ExactKeyOf
, you get what you expect.
type ExactKeyOf<T> = T extends Record<infer U, any> ? U : keyof T;
Examples:
type StringKey = ExactKeyOf<Record<string, any>>; // string type NumberKey = ExactKeyOf<Record<number, any>>; // number type SymbolKey = ExactKeyOf<Record<symbol, any>>; // symbol type ObjectKey = ExactKeyOf<{ a: 1; b: 2 }>; // 'a' | 'b' type UnionKey = ExactKeyOf<{ a: 1; b: 2 } | { c: 3; d: 4 }>; // 'a' | 'b' | 'c' | 'd'
Const Assertions
When you have a literal type, you can use as const
to assert that the type is a literal type and not a general type.
const x = 'hello'; // ^? Type: string const y = 'hello' as const; // ^? Type: 'hello'
This might be quite well-known, but I’ve still seen people not using it. Documentation only exists in the release notes of TypeScript 3.4! So, I’m including it here.
Correlated Union Types
A tagged union is a union where each member has a tag that distinguishes it from the others. This is useful for modeling state machines, for example:
type LoadingState = { state: 'loading' }; type SuccessState<T> = { state: 'success'; data: T }; type ErrorState<T extends Error> = { state: 'error'; error: T }; type Response<T> = Loading | Success<T> | Error<T>;
This allows you to easily switch on the state
property to narrow the type:
const response: Response<number> = ... switch (response.state) { case 'loading': return 'Loading...'; case 'success': return response.data; case 'error': return response.error.message; }
Correlated union types are tagged unions with shared keys between the members, but the types of the shared keys are different. For example:
type Button = { type: 'button'; color: 'red' | 'blue' }; type Link = { type: 'link'; color: 'green' | 'yellow' }; type Element = Button | Link; function getColor(element: Element) { switch (element.type) { case 'button': return element.color; case 'link': return element.color; } } const color = getColor({ type: 'button', color: 'red' }); // ^? Type: 'red' | 'blue' | 'green' | 'yellow'
In this case, to narrow the type, you need to use generics. For example:
function getColor<T extends Button | Link>(element: T): T['color'] { return element.color; } const color = getColor({ type: 'button', color: 'red' }); // ^? Type: 'red'
Similarly, there are times when you want to inform TypeScript that a property cannot exist in a type. For example:
type RenderList<T> = | { items: string[]; } | { items: T[]; renderItem: (item: T) => string; };
In this case, renderItem
should only exist when items
is an array of T
. You can use correlated union types to model this:
type RenderList<T> = | { items: string[]; renderItem?: never; } | { items: Exclude<T, string>[]; renderItem: (item: Exclude<T, string>) => string; };
Compared to the previous example, you will get fewer errors because now TypeScript knows that renderItem
should only exist when items
is not an array of strings. Users can’t accidentally add renderItem
, allowing better type safety.
Conclusion
These are just some of the types and patterns I’ve found useful, but TypeScript features grow with each release. Learning these functionalities take time and practice, so don’t worry if you don’t understand them immediately. Keep coding and experimenting, and you’ll get there!