Typescript tips by Matt Pocock

September 1, 2022

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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!

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.

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.

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 #15 πŸ”₯

You can use generics to dynamically specify the number, and type, of arguments to functions.

Here, we create a sendEvent function which only asks for a payload if it's present on the event you're sending.

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