My TypeScript cheat-sheet
Table of contents
TypeScript became essential part of my professional activity. It got to the point that if I see pure JavaScript code, I simply can't acknowledge that. I immediately start asking myself "How is that possible for a language to not have types?".
There are some tips and tricks I don't use quite often, so I decided to write down some notes, so I could always have them available.
I am not a huge fan of interfaces and inheritance, since there is plenty of ways to create a derivative type, via "union" and a few extra tricks:
type CarType = {speed: number;color: string;model: string;};// Add a subset of properties from CarTypetype PlaneTypeA = {class: string} & Pick<CarType, 'speed' | 'model'>;// Add one property from CarTypetype PlaneType = {class: string;speed: CarType["speed"];};// "Inherit" typetype SuperCarType = {super: boolean;} & CarType;
Not a huge sucker for OOP either, but occasionally I have to deal with it. So,
interface CommandProcessorInstance {}export interface CommandProcessor {new (): CommandProcessorInstance;attach(program: CommanderCommand): void;process(args?: CommandActionArguments): Promise<void>;}export function Implements<T>() {return <U extends T>(constructor: U) => {// eslint-disable-next-line no-unused-expressionsconstructor;};}@Implements<CommandProcessor>()export class CommandRun {public static attach(program: CommanderCommand,) {// ...}public static async process(args: CommandActionArguments,) {// ...}}
type MyMappedObjectType = {[k in string | number]: unknown;};type MyIndexedType = {[k: string]: unknown;};type MyRecord = Record<string, unknown>;
If I have multiple functions of the same signature, I don't have to specify argument types every time. Instead, I make a type declaration once and just assign it to many functions. TS is smart enough to figure argument types in this case.
type SomeFunctionType = (foo: number, bar: string, baz: boolean) => void;const funA: SomeFunctionType = (a, b, c) => { ... };const funB: SomeFunctionType = (a, b, c) => { ... };const funC: SomeFunctionType = (a, b, c) => { ... };
There is a nice article about that (old version).
Generics is a powerful instrument for many cases.
You can define dependencies between function argument types and the result type:
const sort = <D>(data: D[], key: string): D[] => {// ...}
You call it as usual, no need to define D, since Typescript will infer it:
const sorted = sort(myData, 'myKey'); // ts knows that sorted is of type `typeof myData`
If there is no way for Typescript to figure the end value of a generic, you need to inform TS about it upon usage:
const extractAttribute = <D>(attributeName: string) => {return doStuff(/* blah */);};const result = extractSomething<HTMLAnchorElement>('href'); // no way to know
You can apply restrictions on what is passed to a generic:
const doStuff = <K extends { mandatoryData: string }>(arg: K) => {// ...};
You can also use generics to make a dependency between types of function arguments:
function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {return propertyNames.map((n) => o[n]);}
Since functional components in React is nothing but regular functions, you can use generics to add dependencies between types of component properties:
import react from "react";type ListProps<E> = Partial<{itemData: E[];renderItem: (entry: E) => React.ReactElement;}>;export function List<E>({ itemData, renderItem }: ListProps<E>) {return (<div>{itemData?.map(item => renderItem?.(item))}</div>);}
Sometimes I can't import types from a third-party library, or I simply don't have types explicitly defined. Nevertheless, there is a way to declare a variable to be of the same type as the other variable:
const data = [{ name: 'Alex', position: 'pilot' },{ name: 'Amos', position: 'tech' },];const crewMember: typeof data[0] | undefined;
Another good example of typeof application is when you have a map like this:
const ref = {foo: 'Foo',bar: 'Bar',};
And then a function that accepts a key from this map. Then, instead of creating a dedicated type called somewhat like MyRefKeyTypes, we can just utilize the following:
const myFunc = (key: keyof typeof ref) => { ... }
Assume you have an array type imported from a third-party library, or just a variable without any typings. What if you wish to get access to its element type? Easy with infer!
const data = [{ name: 'Alex', position: 'pilot', customField: '1' },{ name: 'Amos', position: 'tech', otherCustomField: true },];type Unpack<T> = T extends (infer U)[] ? U : any;type MyElementType = Unpack<typeof data>;
Same goes with the return value of a function. Consider this:
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;const fetchCake = async () => {return {name: 'Brownie',tastiness: 1,};};type FetchCakeReturnType = Awaited<ReturnType<typeof fetchCake>>;
Let's say you have some type or interface Animal, and several sub-types (sub-interfaces): Cow and Lion. Then, you may want to make a set of checker functions to distinguish between those two. Then you can have it defined like this:
const isCow = (animal: Animal): animal is Cow => { /* ... whatever you check to make sure this is a cow, return boolean */ };const isLion = (animal: Animal): animal is Lion => { /* ... whatever you check to make sure this is a lion, return boolean */ };
Then later on in the code when you apply this check with an if statement, inside of this statement Typescript will infer the right sub-type:
if (isLion(animal)) {animal.roar(); // ts knows that animal is a Lion}if (isCow(animal)) {animal.moo(); // ts knows that animal is a Cow}
Essentially when I do const keys = Object.keys(someStructure); I expect keys to be of types keyof typeof someStructure. Unfortunately, it does not work like that. To fix the issue, I must use type casting:
const keys = Object.keys(someStructure) as (keyof typeof someStructure)[];
Sometimes a third party library may have errors in typings, so you have to rely on manual type casing quite heavily. Sometimes TS does not allow you to typecast due to some reasons. To convince TS that black is white, use casting over the unknown type:
Black as unknown as White
There is a way to make some properties mutually exclusive when declaring a type:
type Disallow<T> = {[key in keyof T]?: never;};type MutuallyExclusive<T, U> = (T & Disallow<U>) | (U & Disallow<T>);type DogOrCow = { walk: () => void; } & MutuallyExclusive<{ bark: () => void; }, { giveMilk: () => void; }>;
You can make an either-either type easily using union in the following way:
type Animals = {kind: "dog";bark: () => void;} | {kind: "cow";moo: () => void;};
Then Typescript looks at the value of kind and make corresponding assumptions.
Every once in a while I need to add custom properties to the window object. The object is backed by the Window interface from TS dom extension. Since interfaces can be extended just by defining a new interface with the same name, we can do this:
declare global {interface Window {myCustomProperty: string;}}
The recursive types are useful to define types that repeat itself internally. Those are extremely powerful in combination with the keyof syntax.
type DeepPartial<T> = T extends object ? {[P in keyof T]?: DeepPartial<T[P]>;} : T;
Sometimes you have a function that has this keyword inside. How to type it? It can be anything: global, document, some other object! There is a way:
type MyCustomObjectType = {fly: () => void;};function doSomething(this: MyCustomObjectType) {this.fly();}
There is also another way:
function doSomething() {const self: MyCustomObjectType = this;self.fly();}
Typescript is a complex language. It has so many features built on-top of the standard EcmaScript. Every day I lean something new when using TS. All my discoveries I publish here, so stay tuned!
Sergei Gannochenko
Golang, React, TypeScript, Docker, AWS, Jamstack.
19+ years in dev.