Avatar

My TypeScript cheat-sheet

← Back to list
Posted on 04.06.2020
Last updated on 12.11.2024
Image by Web Donut on Unsplash
Refill!

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.

# 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;
The code is licensed under the MIT license

# 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,
) {
// ...
}
}
The code is licensed under the MIT license

# Indexed vs Mapped Object vs Record

type MyMappedObjectType = {
[k in string | number]: unknown;
};
type MyIndexedType = {
[k: string]: unknown;
};
type MyRecord = Record<string, unknown>;
The code is licensed under the MIT license

# 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) => { ... };
The code is licensed under the MIT license

# 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[] => {
// ...
}
The code is licensed under the MIT license

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`
The code is licensed under the MIT license

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
The code is licensed under the MIT license

You can apply restrictions on what is passed to a generic:

const doStuff = <K extends { mandatoryData: string }>(arg: K) => {
// ...
};
The code is licensed under the MIT license

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]);
}
The code is licensed under the MIT license

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>
);
}
The code is licensed under the MIT license

# 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;
The code is licensed under the MIT license

Another good example of typeof application is when you have a map like this:

const ref = {
foo: 'Foo',
bar: 'Bar',
};
The code is licensed under the MIT license

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) => { ... }
The code is licensed under the MIT license

# Pulling out the internal type

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>;
The code is licensed under the MIT license

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>>;
The code is licensed under the MIT license

# 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): 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 */ };
The code is licensed under the MIT license

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
}
The code is licensed under the MIT license

# 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)[];
The code is licensed under the MIT license

# 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
The code is licensed under the MIT license

# 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; }>;
The code is licensed under the MIT license

# 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;
};
The code is licensed under the MIT license

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;
}
}
The code is licensed under the MIT license

# 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;
The code is licensed under the MIT license

# 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();
}
The code is licensed under the MIT license

There is also another way:

function doSomething() {
const self: MyCustomObjectType = this;
self.fly();
}
The code is licensed under the MIT license

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!


Avatar

Sergei Gannochenko

Business-oriented fullstack engineer, in ❤️ with Tech.
Golang, React, TypeScript, Docker, AWS, Jamstack.
19+ years in dev.