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

Create types from types

Generics

It is possible to 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>;

«Is» keyword

So the is keyword is really powerful. 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; }>;

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.