Peter Mekhaeil

Typescript tips by Matt Pocock

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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