Typescript tips by Matt Pocock

Sep 1, 2022

16 min. read

Matt Pocock posted a collection of videos on TypeScripts tips that I recommend everyone to watch. These are my notes of each tip that he shared.

List of tips

Tip #1: Derive a union type from an object

馃敟 TypeScript tip 馃敟

Learn how to derive a union type from an object - an incredibly useful switcheroo that's at the heart of most TS magic out there. https://t.co/NeU2DrsjwU

Notes

export const fruitCounts = {
apple: 1,
pear: 4,
banana: 26
};

type FruitCounts = typeof fruitCounts;

type SingleFruitCount = {
[K in keyof FruitCounts]: {
[K2 in K]: number;
};
}[keyof FruitCounts];

const singleFruitCount: SingleFruitCount = {
apple: 2
};

Tip #2: Transform a union to another union using the in operator

馃敟 TypeScript Tip #2 馃敟

Transform a union to another union, using the 'in' operator as a kind of for-loop.

This pattern can be used for almost any kind of transformation - here, I add a dynamic key. https://t.co/xuyNQMYRhY

Notes

export type Entity =
| {
type: 'user';
}
| {
type: 'post';
}
| {
type: 'comment';
};

type EntityWithId = {
[EntityType in Entity['type']]: {
type: EntityType;
} & Record<`${EntityType}Id`, string>;
}[Entity['type']];

const resultComment: EntityWithId = {
type: 'comment',
commentId: '123'
};

const resultPost: EntityWithId = {
type: 'post',
postId: '123'
};

Tip #3: String interpolation

馃敟 TypeScript Tip #3 馃敟

TypeScript's string interpolation powers are incredible, especially since 4.1. Add some utilities from ts-toolbelt, and you've got a stew going.

Here, we decode some URL search params AT THE TYPE LEVEL. https://t.co/d3Khbzj72K

Notes

import { String, Union } from 'ts-toolbelt';

const query = `/home?a=foo&b=wow`;

type Query = typeof query;

type SecondQueryPart = String.Split<Query, '?'>[1];

type QueryElements = String.Split<SecondQueryPart, '&'>;

type QueryParams = {
[QueryElement in QueryElements[number]]: {
[Key in String.Split<QueryElement, '='>[0]]: String.Split<
QueryElement,
'='
>[1];
};
}[QueryElements[number]];

const obj: Union.Merge<QueryParams> = {
a: 'foo',
b: 'wow'
};

Tip #4: Function overloads

馃敟 TypeScript Tip #4 馃敟

Function overloads can be used in conjunction with generics to make incredibly complex and dynamic type signatures.

Here, we make a compose function - incredibly useful for functional programming. https://t.co/STjFkjTKK4

Notes

function compose<Input, FirstArg>(
func: (input: Input) => FirstArg
): (input: Input) => FirstArg;

function compose<Input, FirstArg, SecondArg>(
func: (input: Input) => FirstArg,
func2: (input: FirstArg) => SecondArg
): (input: Input) => SecondArg;

function compose<Input, FirstArg, SecondArg, ThirdArg>(
func: (input: Input) => FirstArg,
func2: (input: FirstArg) => SecondArg,
func3: (input: SecondArg) => ThirdArg
): (input: Input) => ThirdArg;

function compose(...args: any[]) {
// Implement later
return {} as any;
}

const addOne = (a: number) => {
return a + 1;
};

const numToString = (a: number) => {
return a.toString();
};

const stringToNum = (a: string) => {
return parseInt(a);
};

// This will work because the return types match the input type
// of the next function.
const addOneToString = compose(addOne, numToString, stringToNum);

// This will NOT work because return type of `numToString` does
// not match the input type of `addOne`.
const stringToNumber = compose(numToString, addOne);

Tip #5: Using extends to narrow the value of a generic

馃敟 TypeScript Tip #5 馃敟

The 'extends' keyword is very powerful in TypeScript. Here, I use it to narrow the value of a generic to enable some beautiful autocomplete/inference. https://t.co/mSpeT1ICIv

Notes

export const getDeepValue = <
Obj,
FirstKey extends keyof Obj,
SecondKey extends keyof Obj[FirstKey]
>(
obj: Obj,
firstKey: FirstKey,
secondKey: SecondKey
): Obj[FirstKey][SecondKey] => {
return {} as any;
};

const obj = {
foo: {
a: true,
b: 2
},
bar: {
c: 'cool',
d: 2
}
};

const result = getDeepValue(obj, 'bar', 'c');

Tip #6: Extract types using infer

馃敟 TypeScript Tip #6 馃敟

Type helpers change the game when it comes to types in your codebase. They help TypeScript infer more from your code - and make your types a lot more readable.

Here, I write my own PropsFrom helper to extract props from any React component. https://t.co/9VUwqDxgtJ

Notes

import React from 'react';

const MyComponent = (props: { enabled: boolean }) => {
return null;
};

class MyOtherComponent extends React.Component<{ enabled: boolean }> {}

type PropsFrom<TComponent> = TComponent extends React.FC<infer Props>
? Props
: TComponent extends React.ComponentClass<infer Props>
? Props
: never;

const props: PropsFrom<typeof MyComponent> = {
enabled: true
};

const otherProps: PropsFrom<typeof MyOtherComponent> = {
enabled: true
};

Tip #7: Using generics and keyof to type Object.keys

馃敟 TypeScript Tip #7 馃敟

馃鈥嶐煉 Beginner/intermediate
馃挕 Generics

The looseness of Object.keys can be a real pain point when using TypeScript. Luckily, it's pretty simple to create a tighter version using generics and the keyof operator. https://t.co/pF26NHcBU4

export const myObject = {
a: 1,
b: 2,
c: 3
};

const objectKeys = <Obj>(obj: Obj): (keyof Obj)[] => {
return Object.keys(obj) as (keyof Obj)[];
};

objectKeys(myObject).forEach((key) => {
console.log(myObject[key]);
});

Tip #8: Using generics in React props

馃敟 TypeScript Tip #8 馃敟

You can use generics in React to make incredibly dynamic, flexible components. Here, I make a Table component with a generic 'items' type. https://t.co/cqm0xPoEHB

interface TableProps<TItem> {
items: TItem[];
renderItem: (item: TItem) => React.ReactNode;
}

export function Table<TItem>(props: TableProps<TItem>) {
return null;
}

const Component = () => {
return (
<Table
items={[
{
id: '1',
name: 'Peter'
}
]}
renderItem={(item) => <div>{item.name}</div>}
/>
);
};

Tip #9: Generics can be 'curried' through functions

馃敟 TypeScript Tip #9 馃敟

Generics can be 'locked in' by function calls, meaning that generics can be 'curried' through functions.

Here, we create a 'key remover' function which can process any generic object. https://t.co/xjhMXhdnqT

export const makeKeyRemover =
<Key extends string>(keys: Key[]) =>
<Obj>(obj: Obj): Omit<Obj, Key> => {
return {} as any;
};

const keyRemover = makeKeyRemover(['a', 'b']);

const newObject = keyRemover({ a: 1, b: 2, c: 3 });

// Only `c` is available:
newObject.c;
// ^? (property) c: number

Tip #10: Throw error messages for type checks

馃敟 TypeScript Tip #10 馃敟

Using a crazy trick I picked up from @AndaristRake, you can throw detailed error messages for type checks.

Here, I move a runtime check in a function to the type level, meaning you get a detailed error if you use it wrong. https://t.co/K3wb350Lrs

Notes

type CheckForBadArgs<Arg> = Arg extends any[]
? 'You cannot compare two arrays using deepEqualCompare'
: Arg;

export const deepEqualCompare = <Arg>(
a: CheckForBadArgs<Arg>,
b: CheckForBadArgs<Arg>
): boolean => {
if (Array.isArray(a) || Array.isArray(b)) {
throw new Error('You cannot compare two arrays using deepEqualCompare');
}

return a === b;
};

deepEqualCompare(1, 1);
// ^? const deepEqualCompare: <number>(a: number, b: number) => boolean

// Below will throw error:
// Argument of type 'never[]' is not assignable to parameter
// of type '"You cannot compare two arrays using deepEqualCompare"'.
deepEqualCompare([], []);

Tip #11: Deep partials

馃敟 TypeScript Tip #11 馃敟

Deep partials are SUPER useful and not natively supported by TypeScript. Here, I use one to help with mocking an entity in a (imaginary) test file. https://t.co/jgKkwTzSKL

Notes

export type DeepPartial<Thing> = Thing extends Function
? Thing
: Thing extends Array<infer InferredArrayMember>
? DeepPartialArray<InferredArrayMember>
: Thing extends object
? DeepPartialObject<Thing>
: Thing | undefined;

interface DeepPartialArray<Thing> extends Array<DeepPartial<Thing>> {}

type DeepPartialObject<Thing> = {
[Key in keyof Thing]?: DeepPartial<Thing[Key]>;
};

interface Post {
id: string;
comments: { value: string }[];
meta: {
name: string;
description: string;
};
}

const post: DeepPartial<Post> = {
id: '1',
meta: {
description: '123'
}
};

Tip #12: Loose autocomplete

馃敟 TypeScript Tip #12 馃敟

Ever wanted just a _bit_ of autocomplete?

Here, we create a TypeScript helped called LooseAutocomplete which gives us autocomplete while also allowing arbitrary values.

Picked up this tip from @GavinRayDev - worth a follow! https://t.co/IziEdyLWkw

Notes

type IconSize = LooseAutocomplete<'sm' | 'xs'>;

type LooseAutocomplete<T extends string> = T | Omit<String, T>;

interface IconProps {
size: IconSize;
}

export const Icon = (props: IconProps) => {
return <></>;
};

const Comp1 = () => {
return (
<>
<Icon size="xs"></Icon>
</>
);
};

Tip #13: Grab types from modules

馃敟 TypeScript Tip #13 馃敟

Want to turn a module into a type? You can use typeof import('./') to grab the type of any module, even third-party ones.

Here, we create a type from a constants.ts file, then map over the values to create a union. https://t.co/NjUzh3AYnq

Notes

// constants.ts
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const EDIT_TODO = 'EDIT_TODO';

// types.ts
export type ActionModule = typeof import('./constants');

export type Action = ActionModule[keyof ActionModule];
// ^? "ADD_TODO" | "REMOVE_TODO" | "EDIT_TODO"

Tip #14: Globals in TypeScript

馃敟 TypeScript Tip #14 馃敟

Globals in TypeScript?! 馃く

declare global is a super useful tool for when you want to allow types to cross module boundaries.

Here, we create a GlobalReducer type, where you can add new event types as you create new reducers. https://t.co/NO1J9aaaTS

Notes

// types.ts
declare global {
interface GlobalReducerEvent {}
}

export type GlobalReducer<TState> = {
state: TState;
event: {
[EventType in keyof GlobalReducerEvent]: {
type: EventType;
} & GlobalReducerEvent[EventType];
}[keyof GlobalReducerEvent]
} => TState;

// todoReducer.ts
import { GlobalReducer } from './types';

declare global {
interface GlobalReducerEvent {
ADD_TODO: {
text: string;
};
}
}

export const todosReducer: GlobalReducer<{
todos: { id: string }[]
}> = (state, event) => {
return state;
};


// userReducer.ts
import { GlobalReducer } from './types';

declare global {
interface GlobalReducerEvent {
LOG_IN: {};
}
}

export const userReducer: GlobalReducer<{ id: string }> = (state, event) => {
// GlobalReducer has the globals from across all reducers:
// event: { type: 'LOG_IN'; } | ({ type: 'ADD_TODO' } & { text: string })
return state;
};

Tip #15: Use Generics to dynamically specify the number and types of function arguments

馃敟 TypeScript Tip #14 馃敟

Globals in TypeScript?! 馃く

declare global is a super useful tool for when you want to allow types to cross module boundaries.

Here, we create a GlobalReducer type, where you can add new event types as you create new reducers. https://t.co/NO1J9aaaTS

Notes

export type Event =
| { type: 'LOG_IN'; payload: { userId: string } }
| { type: 'SIGN_OUT' };

const sendEvent = <Type extends Event['type']>(
...args: Extract<Event, { type: Type }> extends { payload: infer TPayload }
? // Named tuple
[type: Type, payload: TPayload]
: [type: Type]
) => {};

sendEvent('SIGN_OUT');
sendEvent('LOG_IN', { userId: '123' });