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
- Derive a union type from an object using dynamic keys. No need to create a union type and specifying each object key manually.
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
- Transform
{ type: 'user' }
to{ type: 'user', userId: string }
Record
supports template literal types.
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
- TS Playground
- Uses ts-toolbelt to split strings and merge objects.
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
- TS Playground
- Add type signatures to a compose function.
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
- Narrow the value of a generic using
extends
.
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
- Extract props from React components using
infer
.
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
- Return an error message as a string type.
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
- Recursively partial down an object to make each key optional.
- Useful for mocking an entity in a test file.
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
- Autocomplete while allowing arbitrary values.
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
- Create type from another module.
- Map over values to create a union.
// 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
- Globals can cross module boundaries
// 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
- Uses
Extract
andinfer
to specify if a type is optional or not. - Give names to
args_0
andargs_1
using named tuple.
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' });