Typescript

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the javascript category.

Last Updated: 2024-11-23

Unions

How to convert an array of entities into a union?

The trick is to use typeof and then index it with the number type:

const sizes = ['small', 'medium', 'large'] as const;

// 👇️ type SizesUnion = "small" | "medium" | "large"
type SizesUnion = typeof sizes[number];

How to remove an item from a Union type.

type T0 = "a" | "b" | "c"
type T1 = Exclude<T0, "a">; // Omit won't work here
// type T1 = "b" | "c"

How to take a union of types and just grab the ones assignable to another union?

type T0 = string | number | (() => void)
// `Function` is just a union of one here
type T1 = Extract<T1, Function>;
// type T1 = () => void

Exclusive OR

Use never for a property on an interface and then the union operator for two mutually exclusively possible prop sets

interface PropsBase {
  headline?: string,
  className?: string,
}

interface PropsWithText extends PropsBase {
  text: string
  children?: never
}

interface PropsWithTextReplacementChildren extends PropsBase {
  text?: never,
  children: React.ReactNode,
}

type Props = PropsWithText | PropsWithTextReplacementChildren;

Strings

Match on the uppercase/lowercase/capitalized variant of a string, use these helpers

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
// type ShoutyGreeting = "HELLO, WORLD"

Combine literal strings and operations, such that only part of the string is changed

type ASCIICacheKey<Str extends string> = `ID-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
type MainID = "ID-my-app"

Assertion Signatures

// Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

How to get types for external packages

You can give TypeScript types for code that exists elsewhere (e.g. written in JavaScript) using the declare keyword.

e.g. if, in your main ts code, you'd like to do

import foo from "foo-module";
foo();

but the foo library has no types, then you can define a "module" with those types

// declarations.d.ts
declare module "foo-module" {
  function foo(): void;
  export = foo;
}

Note that this does not define the actual functionality - just types for it.

declare var foo: any;

// In actual code
foo = 123; // allowed

If you want to use a namespace from a package as a type, import the library (eg. import * as firebaseNamespace from firebase) and then call typeof firebaseNamespace for typing.

Forcing types with as - and the "unknown" trick

If you know more about a type than typescript does, you can force it to interpret a type a certain way - e.g. when it complained that timerId might be a boolean, you could do clearInterval(timerId as unknown as number);

The bit with the as unknown in the center is necessary in cases where the types have insufficient overlap.

Inference is impossible when initializing useState with null

The following will wrongly infer the type of timerId to be null

const [timerId, setTimerId] = useState(null)

How to make it realize it could be number too?

const [timerId, setTimerId] = useState<number | null>(null)

Object

General object types

Due to JS weirdness, don't use object as a type since it just means "any non-nullish value". Instead use Record<string, unknown> or a description of the object itself:

type PersonObj = {
  name: string,
  age: number,
  subscribed?: boolean
}

How to dynamically construct an object type from a known fixed list of keys

type CatName = "miffy" | "boris" | "mordred";

interface CatInfo {
  age: number;
  breed: string;
}

const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

Type equal to one of the keys to an object

const person = {age: 10, name: "jack"}
type PersonKey = keyof typeof person
// type PersonKey = "age" | "name|

Make all properties optional (e.g. for doing a PATCH update to the API)

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

Make all fields (that might be optional) mandatory (opposite of Partial)

interface Props {
  a?: number;
  b?: string;
}

const obj2: Required<Props> = { a: 5 };
// Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

How to just use a white-listed subset of properties from a starting type?

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

// Just expect the "title" and "completed" properties
type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

How to take all the keys of a property, except for a few black-listed ones

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

// Everything except `description`
type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  createdAt: 1615544252770,
};

Linting

Use typescript-eslint to prevent stupidness.

Function types

If multiple functions take the same types, you can define a type for that function

type TakePhotoCallback = ({exif, uri}: {exif?: Record<string,unknown>, uri: string}) => unknown;

// Notice that this type definition is _used_ after the initial variable name (not after the
// function arguments)
const saveData: TakePhotoCallback = async ({exif, uri})=> {
}

Overriding types

Sometimes typescript has the wrong interface built in and you have to override it. This involves creating a global.d.ts file, including the overrides

declare global {
  interface FormDataValue {
    uri: string;
    name: string;
    type: string;
  }

  interface FormData {
    append(name: string, value: FormDataValue, fileName?: string): void;
    set(name: string, value: FormDataValue, fileName?: string): void;
  }
}

// I don't know why, but this last line was necessary
export {};

Then import this file in the main entry point for your code

import "./global.d.ts"

Promises / Async

If you have then() chain, your type will be Promise<number> - where number is replaced with whatever then returns.

To get the awaited item, wrap in Awaited, e.g. Awaited<Promise<number>> will be number.

Genetic Types

Defaults for Generic Types

You can give default types to generic type definitions using T = DefaultType

type Classification = {category: string, probability: number};
type PostResponse<T = Classification> = {data: T, status: number};

Functions

How to get type of the params to a function

function centerMap(lng: number, lat: number) { }

type CenterMapParams = Parameters<typeof centerMap>
// type CenterMapParams = [lng: number, lat: number]

How Get the type of the return value of a function (e.g. a React hook)

export type UsePushNotificationsReturnValue = ReturnType<typeof usePushNotifications>

How to get a sub-field from the return value of a function

Say you have this function

useLocation() {
  return {
    x: 20,
    getGeolocation,
  }
}

type UseLocationProps = ReturnType<typeof useLocation>
// type UseLocationProps = {x: number, getGeolocation: () => void]

You need to access with ['X'] notation, not .X notation

ReturnType<typeof useLocation>['getGeolocation]`

Null etc

How to exclude null and undefined from a type

type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number

Resources