Avatar

My TypeScript cheat-sheet

Photo by Web Donut on Unsplash
Refill!

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.

Extend one complex type from another

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 CarType
type PlaneTypeA = {
class: string
} & Pick<CarType, 'speed' | 'model'>;
// Add one property from CarType
type PlaneType = {
class: string;
speed: CarType["speed"];
};
// "Inherit" type
type SuperCarType = {
super: boolean;
} & CarType;

Typing static class methods

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-expressions
constructor;
};
}
@Implements<CommandProcessor>()
export class CommandRun {
public static attach(
program: CommanderCommand,
) {
// ...
}
public static async process(
args: CommandActionArguments,
) {
// ...
}
}

Indexed vs Mapped Object vs Record

type MyMappedObjectType = {
[k in string | number]: unknown;
};
type MyIndexedType = {
[k: string]: unknown;
};
type MyRecord = Record<string, unknown>;

Assign argument types

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) => { ... };

Generics

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]);
}

Typeof

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) => { ... }

Pulling out the type of an element of an array

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>;

Type narrowing

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): is Cow => { /* ... whatever you check to make sure this is a cow, return boolean */ };
const isLion = (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
}

Object.keys() problem

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)[];

Dealing with poor typings

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

Mutually excluded properties in a type literal

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; }>;

Mutually exclusive type with union

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.

Extend global objects

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;
}
}

Recursive types

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;

Typing "this" in a function

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 different 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!


Avatar

Sergei Gannochenko

Business-oriented fullstack engineer, in ❤️ with Tech.
React, Node, Docker, AWS, Jamstack.
15+ years in dev.