[LS] TypeScript Concepts + Examples

Table of contents

Before jumping into the actual content, this post (and the upcoming ones) is one of the cheat sheets I keep in my Notion account. Since these notes were intended for personal use, the writing style is such that it is easy for me to understand. I want to share them online in case someone finds them useful.


Let's say you wanted a cheat sheet with some practical TypeScript concepts and examples.


Disclaimer: These notes were made following the Total TypeScript workshops by Matt Pocock.

Extract a promise's result

// Use utility types To Extract a Promise's Result

// Example
function getPet(id: number) {
  if (id === 1) {
    return 'Dog';
  }
  return 'Cat';
}

type GetPetPromise = ReturnType<typeof getPet>
type ReturnValue = Awaited<GetUserPromise>;

// Or
type ReturnValueV2 = Awaited<ReturnType<typeof getPet>>

Extract function parameters

// The Parameters utility is really useful for extracting type
// information that you don't necessarily have control of,
// such as code in external libraries.

// Example
function myFunction(param1: string, param2: number) {
  return null;
}

type MyFunctionType = typeof myFunction
type MyFunctionParameters = Parameters<MyFunctionType>;

// Or
type MyFunctionParametersV2 = Parameters<typeof myFunction>;


// Extract type of the second parameter of the function
type MyFunctionParametersSecondArgument = MyFunctionParametersV2[1];

Create unions from objects using keyof

const frontEndFrameworks = {
  react: {
    label: "React",
  },
  vue: {
    label: "Vue",
  },
  svelte: {
    label: "Svelte",
  },
};

// This gives you 'react' | 'vue' | 'svelte'
type FrontEndFramework = keyof typeof frontEndFrameworks;

Discriminated union types

// A common technique for working with unions is to have a single field
// which uses literal types which you can use to let TypeScript narrow
// down the possible current type.

type NetworkLoadingState = {
  state: "loading";
};

type NetworkFailedState = {
  state: "failed";
  code: number;
};

type NetworkSuccessState = {
  state: "success";
  response: {
    type: string
    value: number
  };
};

// Create a type which represents only one of the above types
// but you aren't sure which it is yet.

// All of the above types have a field named `state`, and then they also have their own fields.
// With `state` as a literal type, you can compare the value of `state` to the equivalent string 
// and TypeScript will know which type is currently being used.
type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState;

Extract from a union using a utility type

export type MusicGenre =
  | {
      type: "soul";
      artist: "Stevie Wonder";
    }
  | {
      type: "hiphop";
      artist: "2Pac";
    }
  | {
      type: "pop";
      artist: "Michael Jackson";
    };

type SoulGenre = Extract<MusicGenre, { type: "soul" }>;


// Extract also works with unions.
type Genres = "soul" | "hiphop" | "pop"
type SoulAndHipHop = Extract<Genres, "soul" | "hiphop">

Remove a single member of a union

type Genres = "soul" | "hiphop" | "pop"
type Pop = Exclude<Genres, "soul" | "hiphop">

Use indexed access types to extract object properties

export const tech = {
  frontend: "javascript",
    backend: {
        php: 1,
        go: 2,
      }
};

type Tech = typeof tech
type FrontendType = Tech["frontend"]
type BackendType = Tech["backend"]["go"]

Infer object's values as Literal Types with as const

// There are a couple useful things that `as const` does for us.
// First, it freezes the object's values and ensures they are inferred 
// as their literal types. It also adds the readonly annotation to the 
// object's keys, which makes the properties immutable.
// Without the the `as const` annotation, the properties are mutable.
// We can also use as const with arrays.


// on hover
// const musicGenresMap: {
   // readonly SOUL: "soul";
    // readonly HIPHOP: "hiphop";
    // readonly POP: "pop";
// }
const musicGenresMap = {
  SOUL: "soul",
  HIPHOP: "hiphop",
  POP: "pop",
} as const

// on hover
// const arr: readonly [1, 2, 3]
const arr = [1, 2, 3] as const


// We can also use as const with deeply nested data.

// on hover
// const anotherMap: {
//   readonly another: {
//       readonly map: "map";
//   };
// }
const anotherMap = {
  another: {
    map: "map"
  }
} as const;

Extract specific members from a union with indexed access

const customModeEnumMap = {
  PERSONALIZED: "personalized",
  PUBLIC_ANNOUNCEMENT: "publicAnnouncement",
  INTERACTIVE_ONE_ON_ONE: "interactiveOneOnOne",
  SOLO_LEARNING: "soloLearning",
  SCHEDULED_PERSONALIZED: "scheduledPersonalized",
  SCHEDULED_SOLO_LEARNING: "scheduledSoloLearning",
} as const;

// type CustomProgramType1 = "interactiveOneOnOne" | "soloLearning" | "scheduledPersonalized" | "scheduledSoloLearning"
type CustomProgramType1 = typeof customModeEnumMap[
  | "INTERACTIVE_ONE_ON_ONE"
  | "SOLO_LEARNING"
  | "SCHEDULED_PERSONALIZED"
  | "SCHEDULED_SOLO_LEARNING"
];

// V2
// type CustomProgramType2 = "interactiveOneOnOne" | "soloLearning" | "scheduledPersonalized" | "scheduledSoloLearning"
export type CustomProgramType2 = typeof customModeEnumMap[
  Exclude<
    keyof typeof customModeEnumMap,
    "PERSONALIZED" | "PUBLIC_ANNOUNCEMENT"
  >
];

// V3. More readable
// type CustomExampleType = "INTERACTIVE_ONE_ON_ONE" | "SOLO_LEARNING" | "SCHEDULED_PERSONALIZED" | "SCHEDULED_SOLO_LEARNING"
type CustomExampleType = Exclude<
  keyof typeof customModeEnumMap,
  "PERSONALIZED" | "PUBLIC_ANNOUNCEMENT"
>;

// type CustomProgramType3 = "interactiveOneOnOne" | "soloLearning" | "scheduledPersonalized" | "scheduledSoloLearning"
type CustomProgramType3 = typeof customModeEnumMap[CustomExampleType];

Get all the values of an object with Obj[keyof Obj]

const carTypesEnumMap = {
  sedan: "SEDAN_TYPE",
  suv: "SUV_TYPE",
  coupe: "COUPE_TYPE",
} as const;

type CarTypes = typeof carTypesEnumMap;
type VehicleType1 = CarTypes[keyof CarTypes]; // "SEDAN_TYPE" | "SUV_TYPE" | "COUPE_TYPE"

// Manually putting the keys inside of CarTypes would yield the same result:
type VehicleType2 = CarTypes["suv" | "coupe" | "sedan"];

Creating a union out of every element of an array

const fruits = ["apple", "banana", "orange"] as const

// Then use typeof fruits to access into the array using an indexed access type with 0 | 1:
type AppleOrBanana = typeof fruits[0 | 1]

// Get all of the Fruits
// Here number acts as any possible number, so we get the union of all 
// of the elements in the array.
type Fruits = typeof fruits[number]

Template literal with strings

// Template literal types let us be really specific with the types of 
// strings that you can pass in.
// Here's how we would format strings that begin with a slash:

type Route = `/${string}`

Extracting string pattern matches with template literals

type Routes = "/users" | "/users/:id" | "/posts" | "/posts/:id"

// In order to extract strings with :id, use the Extract utility.
// This says "a string of any length, followed by a colon, followed 
// by a string of any length."
// Combining the Extract syntax and our template, our solution 
// looks like this:
type DynamicRoutes = Extract<Routes, `${string}:${string}`>

// Using template literals this way is like using a RegEx.

Passing unions into template literals

type BreadType = "rye" | "brown" | "white";
type Filling = "cheese" | "ham" | "salami";

// This returns all the type combinations (9) of a BreadType with a 
// Filling
type Sandwich = `${BreadType} sandwich with ${Filling}`;

Use a utility type to create an object from a union


type TemplateLiteralKey = `${"user" | "post" | "comment"}${"Id" | "Name"}`;

type ObjectOfKeys = Record<TemplateLiteralKey, string>;

// ObjectOfKeys result
// type ObjectOfKeys = {
//   userId: string;
//   userName: string;
//   postId: string;
//   postName: string;
//   commentId: string;
//   commentName: string;
// }

Manipulate string literals using type helpers

type Event = `log_in` | "log_out" | "sign_up";

// type ObjectOfKeys1 = {
//   LOG_IN: string;
//   LOG_OUT: string;
//   SIGN_UP: string;
// }
type ObjectOfKeys1 = Record<Uppercase<Event>, string>;

// type ObjectOfKeys2 = {
//   log_in: string;
//   log_out: string;
//   sign_up: string;
// }
type ObjectOfKeys2 = Record<Lowercase<Event>, string>;


// type ObjectOfKeys3 = {
//   Log_in: string;
//   Log_out: string;
//   Sign_up: string;
// }
type ObjectOfKeys3 = Record<Capitalize<Event>, string>;

Use constraints with extends to limit type parameters

// Adding `extends string` to the type helper tells it that TRoute must be a string
type AddRoutePrefix<TRoute extends string> = `/${TRoute}`;

Support function type constraints with variable arguments

// This means that we can accept any number of arguments, including 
// zero, and it will return a function or anything from that function.
type GetParametersAndReturnType<T extends (...args: any) => any> = {}

Exclude null and undefined from the Maybe type


// Everything is an object. You can use an empty object to represent 
// anything that isn't null or undefined.
export type Maybe<T extends {}> = T | null | undefined;

Enforce a minimum array length in a type helper

// The first T enforces that there is at least one item in the array, 
// while supporting however many more items may be there thanks to the 
// `rest` parameter.
type NonEmptyArray<T> = [T, ...Array<T>];

Compare and return values with extends and the ternary operator

type CatOrDog<T> = T extends "cat" ? "cat" : "dog";

Infer elements inside a conditional with infer

// The goal is to be able to pass an object with a data attribute into GetDataValue 
// and have it return whatever value is there

// Solution #1
type GetDataValue<T> = T extends { data: any } ? T["data"] : never;

// Solution #2 (preferred)
type GetDataValue<T> = T extends { data: infer TData }
  ? TData
  : never;

// The `infer` in T extends { data: infer TData } says 
// "Whatever is passed into the data key, infer its type".
// Then, the `infer` declares TData for the true branch. 
// If we try and access TData in the 'false' branch, we won't be able to.
// In other words, the TData variable is only defined for one branch.

Extract type arguments to another type helper

interface MyComplexInterface<Event, Context, Name, Point> {
  getEvent: () => Event;
  getContext: () => Context;
  getName: () => Name;
  getPoint: () => Point;
}

type GetPoint<T> = T extends MyComplexInterface<any, any, any, infer TPoint>
  ? TPoint
  : never;

type Example = MyComplexInterface<
  "click",
  "window",
  "my-event",
  { x: 12; y: 14 }
>;

// Returns { x: 12; y: 14 }
type Example2 = GetPoint<Example>;

Pattern matching on template literals with infer


// There are two solutions to extracting the last names from the 
// template literals in the Names tuple:

import { S } from 'ts-toolbelt'

type Names = [
  "Jimi Hendrix",
  "BB King"
]

// Solution 1: Using S.Split
type GetSurname<T extends string> = S.Split<T, " ">[1]

// Solution 2: Using infer
type GetSurname<T> = T extends `${infer First} ${infer Last}` ? Last : never

// Here, we use the conditional check to check that T is a string with
// two strings with a space between, then infer the strings in those 
// slots.

// We could just use `extends ${string} ${infer Last}` because we 
// don't care about the Firstname

Extract the result of an async function

const getServerSideProps = async () => {
  const data = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const json: { title: string } = await data.json();
  return {
    props: {
      json,
    },
  };
};

type InferPropsFromServerSideFunction<T> = T extends () => Promise<{
  props: infer P
}>
  ? P
  : never

Extract the result from several possible function shapes

const parser1 = {
  parse: () => 1,
};

const parser2 = () => "123";

const parser3 = {
  extract: () => true,
};


// Solution #1
type GetParserResult<T> = T extends {
  parse: () => infer TResult;
} ? TResult
  : T extends () => infer TResult
  ? TResult
  : T extends {
      extract: () => infer TResult;
    }
  ? TResult
  : never;


// Solution #2 (better)
type GetParserResult<T> = T extends
  | {
      parse: () => infer TResult
    }
  | {
      extract: () => infer TResult
    }
  | (() => infer TResult)
  ? TResult
  : never

Map οver a union to create an object

// The RoutesObject should be an object where both the keys and 
// the values are equal to the members of the union.

// type RoutesObject = {
//  "/": "/"
//  "/about": "/about"
// }
type RoutesObject = {
  [R in Route]: R
}

Mapped types with objects

interface Attributes {
  firstName: string
  lastName: string
  age: number
}

// type AttributeGetters = {
//  id: () => number;
//  name: () => string;
//  age: () => number;
// };
type AttributeGetters = {
  [K in keyof Attributes]: () => Attributes[K]
}

Transforming object keys in mapped types

// Remap the key to a new one that it is prefixed with get as shown:

interface Attributes {
  firstName: string
  lastName: string
  age: number
}

// type AttributeGetters = {
//   getFirstName: () => string;
//   getLastName: () => string;
//   getAge: () => number;
// }
type AttributeGetters = {
  [K in keyof Attributes as `get${Capitalize<K>}`]: () => Attributes[K]
}

Conditionally extract properties from an object

interface Example {
  name: string
  age: number
  id: string
  organisationId: string
  groupId: string
}

// This will use a string template to look for "id" or "Id" that is
// surrounded by strings of any length:
type SearchForId = `${string}${"id" | "Id"}${string}`


// For every key of the object, we’ll check if it extends SearchById. 
// If it does, it will be included. Otherwise, the never type will be 
// used. By using never as the else case, any keys that don’t extend 
// SearchById will not be included in the OnlyIdKeys object.
type OnlyIdKeys<T> = {
  [K in keyof T as K extends SearchForId ? K : never]: T[K]

Map a discriminated union to an object

type Route =
  | {
      route: "/";
      search: {
        page: string;
        perPage: string;
      };
    }
  | { route: "/about"; search: {} }
  | { route: "/admin"; search: {} }


// Solution #1
type RoutesObject = {
  [R in Route["route"]]: Extract<Route, { route: R }>["search"];
};

// Solution #2 (better)

/**
 * Here, R represents the individual Route. The thing we're iterating 
 * DOESN'T have to be a string | number | symbol, as long as the
 * thing we cast it to is.
 */
type RoutesObject = {
  [R in Route as R["route"]]: R["search"];
};

// Result
// type RoutesObject = {
//   "/": {
//       page: string;
//       perPage: string;
//   };
//   "/about": {};
//   "/admin": {};
// }

Map an object to a union of tuples

// Create a Union of Tuples by Re-indexing a Mapped Type

interface Values {
  email: string;
  firstName: string;
  lastName: string;
}

type ValuesAsUnionOfTuples = {
  [K in keyof Values]: [K, Values[K]]
}[keyof Values]

// With [keyof Values] appended, we are creating an intermediary object that contains the values we want, but then
// remapping over it using its key.
// This results in the union of tuples we wanted:
// type ValuesAsUnionOfTuples = ['email', string] | ['firstName', string] | ['lastName', string],

Transform an object into a union of template literals

interface FruitMap {
  apple: "red";
  banana: "yellow";
  orange: "orange";
}

type TransformedFruit = {
  [K in keyof FruitMap]: `${K}:${FruitMap[K]}`
}[keyof FruitMap]

// on hover
// type TransformedFruit = `apple:red` | `banana:yellow` | `orange:orange`

Transform a discriminated union into a union

type Fruit =
  | {
      name: "apple";
      color: "red";
    }
  | {
      name: "banana";
      color: "yellow";
    }
  | {
      name: "orange";
      color: "orange";
    };

type TransformedFruit = {
  [F in Fruit as F["name"]]: `${F["name"]}:${F["color"]}`;
}[Fruit["name"]];

// on hover
// type TransformedFruit = "apple:red" | "banana:yellow" | "orange:orange"

Transform path parameters from strings to objects

import { S } from 'ts-toolbelt'

type UserPath = "/users/:id";
type UserOrganisationPath = "/users/:id/organisations/:organisationId";

type ExtractPathParams<TPath extends string> = {
  [K in S.Split<TPath, "/">[number] as K extends `:${infer P}`
    ? P
    : never]: string;
};


// on hover
// Result: { id: string }
type Result = ExtractPathParams<UserPath>

// on hover
// Result: { id: string, organisationId: string }
type Result = ExtractPathParams<UserOrganisationPath>

Transform an object into a discriminated union

type MutuallyExclusive<T> = {
  [K in keyof T]: Record<K, T[K]>;
}[keyof T];

Transform a discriminated union with unique values to an object

type Route =
  | {
      route: "/";
      search: {
        page: string;
        perPage: string;
      };
    }
  | { route: "/about" }
  | { route: "/admin" }
  | { route: "/admin/users" };


// on hover
// {
//      "/": { page: string; perPage: string; };
//      "/about": never;
//      "/admin": never;
//      "/admin/users";
// }
type RoutesObject = {
  [R in Route as R["route"]]: R extends { search: infer S } ? S : never;
};

Construct a deep partial of an object

// Use Recursion and Mapped Types to Create a Type Helper

type DeepPartial<T> = T extends Array<infer U>
  ? Array<DeepPartial<U>>
  : { [K in keyof T]?: DeepPartial<T[K]> };

Add object property constraints to a generic function

type TUser = { firstName: string; lastName: string }

export const concatenateFirstNameAndLastName = <T extends TUser>(user: T) => {
  return {
    ...user,
    fullName: `${user.firstName} ${user.lastName}`,
  }
}

// on hover
const newUsers: ({
  id: number;
  firstName: string;
  lastName: string;
} & {
  fullName: string;
})[]

Use generics to type a fetch request

// Solution #1
const fetchData = async <TData>(url: string) => {
  let data = await fetch(url).then((response) => response.json());

  return data as TData;
};


// Solution #2
const fetchData = async <TData>(url: string) => {
  let data: TData = await fetch(url).then((response) => response.json());

  return data;
};

Represent generics at the lowest level

export const getHomePageFeatureFlags = <HomePageFlags>(
  config: {
    rawConfig: {
      featureFlags: {
        homePage: HomePageFlags;
      };
    };
  },
  override: (flags: HomePageFlags) => HomePageFlags
) => {
  return override(config.rawConfig.featureFlags.homePage);
};

// Using HomePageFlags directly as the generic makes for a more elegant solution since it is the argument for the override function.
// We can now drill down to config.rawConfig.featureFlags.homePage inside of the argument

Typed object keys

const typedObjectKeys = <TKey extends string>(obj: Record<TKey, any>) => {
  return Object.keys(obj) as Array<TKey>;
};

// Hovering over the any shows us that the generic slot contains the "a" or "b"
// const typedObjectKeys: <"a" | "b">(obj: Record<"a" | "b", any>) => ("a" | "b")[]

Inferring literal types from any basic type

export const inferItemLiteral = <T extends string | number>(t: T) => {
  return {
    output: t,
  }
}

// Type of result1 is { output: "a" }
const result1 = inferItemLiteral("a")

Infer the type of an array member

// before
const makeStatus1 = <TStatuses extends string[]>(statuses: TStatuses) => {
    return statuses;
}

// const statuses: string[]
const statuses1 = makeStatus1(["INFO", "DEBUG", "ERROR", "WARNING"]);

// after:
const makeStatus2 = <TStatuses extends string>(statuses: TStatuses[]) => {
    return statuses;
}

// const statuses2: ("INFO" | "DEBUG" | "ERROR" | "WARNING")[]
const statuses2 = makeStatus2(["INFO", "DEBUG", "ERROR", "WARNING"]);

Ensure runtime level & type level safety with conditional types

// Solution #1

// This is a common error people see when using conditional types in return types, 
// because TypeScript isn't smart enough to match the runtime code 
// to the type level code. To fix it, we need to add an `as any` to the 
// return.

function youSayGoodbyeISayHello<TGreeting extends "hello" | "goodbye">(
  greeting: TGreeting,
): TGreeting extends "hello" ? "goodbye" : "hello" {
  return (greeting === "goodbye" ? "hello" : "goodbye"); as any
}

// Solution #2 - Adding a Type Helper
type GreetingResult<TGreeting> = TGreeting extends "hello"
  ? "goodbye"
  : "hello";

function youSayGoodbyeISayHello<TGreeting extends "hello" | "goodbye">(
  greeting: TGreeting,
) {
  return (
    greeting === "goodbye" ? "hello" : "goodbye"
  ) as GreetingResult<TGreeting>;
}

Function overloads vs. conditional types

// Remember that in order to create function overloads you need to 
// use the function keyword, not the const func = () => {}

function youSayGoodbyeISayHello(greeting: "goodbye"): "hello";
function youSayGoodbyeISayHello(greeting: "hello"): "goodbye";

// A good way to increase type safety when using function overloads is to add the return types to
// the original function and make sure the types line up with your overloads:
function youSayGoodbyeISayHello(
  greeting: "goodbye" | "hello"
): "goodbye" | "hello" {
  return greeting === "goodbye" ? "hello" : "goodbye";
}

Specifying types for an overloaded function

function getRolePrivileges(role: "admin"): AdminPrivileges;
function getRolePrivileges(role: "user"): UserPrivileges;
function getRolePrivileges(role: string): AnonymousPrivileges;
function getRolePrivileges(
  role: string,
): AnonymousPrivileges | AdminPrivileges | UserPrivileges {
  switch (role) {
    case "admin":
      return {
        sitesCanDelete: [],
        sitesCanEdit: [],
        sitesCanVisit: [],
      };
    case "user":
      return {
        sitesCanEdit: [],
        sitesCanVisit: [],
      };
    default:
      return {
        sitesCanVisit: [],
      };
  }
}

Create a function with a dynamic number of arguments

interface Events {
    click: {
        x: number;
        y: number;
    };
    focus: undefined;
}

export const sendEvent = <TEventKey extends keyof Events>(
    event: TEventKey,
    ...args: Events[TEventKey] extends undefined ? [] : [payload: Events[TEventKey]]
) => {
    //Send the event somewhere
};

// Adding payload to the Events[TEventKey] tuple tells TypeScript 
// we want to call the result payload

Modify a generic type default for improved error messages

// To make sure unknown is never available, we can change the default
// of TResult in the type argument to be the error message:
const fetchData = async <
  TResult = "You must pass a type argument to fetchData"
>(
  url: string
): Promise<TResult> => {
  const data = await fetch(url).then((response) => response.json())
  return data
}

Use declaration merging to add functionality to the global window

// We'll take advantage of declaration merging by declaring a Window interface, which will contain 
// the makeGreetingSolution function. This means that makeGreetingSolution is now being stuffed into 
// the globally available Window without being part of the globalThis.

// When multiple interfaces are declared with the same name, their properties will all be required.
// This is called declaration merging.
// Note that declaration merging does not work with types.
declare global {
  interface Window {
    makeGreetingSolution: () => string;
  }
}

Typing process.env in the nodeJS Namespace

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      MY_SOLUTION_ENV_VAR: string;
    }
  }
}

Use a type predicate to filter types

// A type predicate expresses a return type by saying that something 
// is something.

const filteredValues = values.filter((value): value is string =>
  Boolean(value),
);

// Type Predicates Must Return Boolean
// It's important to note that when using a type predicate you have 
// to return a Boolean.

Using const type parameters for better inference


// Now when this function is called with a tuple of elements, it infers the entire tuple of elements.
// For example, if we have name: "apple" and name: "banana", it will be inferred as a literal:
// hovering over `const fruits = asConst([...
export const asConst = <const T>(t: T) => t;


// Example
const fruits = asConst([
  {
    name: "apple",
    price: 1,
  },
  {
    name: "banana",
    price: 2,
  },
]);

// on hover
const asConst: <readonly [{
  readonly name: "apple";
  readonly price: 1;
}, {
  readonly name: "banana";
  readonly price: 2;
}
}]>(t: readonly [{
  readonly name: "apple";
  readonly price: 1;
}, {
  readonly name: "banana";
  readonly price: 2;
} 
}]) => readonly [{
  readonly name: "apple";
  readonly price: 1;
}, {
  readonly name: "banana";
  readonly price: 2;
}
}]

Add objects to the global scope dynamically

const addAllOfThisToWindow = {
  addSolution: (a: number, b: number) => a + b,
  subtractSolution: (a: number, b: number) => a - b,
  multiplySolution: (a: number, b: number) => a * b,
  divideSolution: (a: number, b: number) => a / b,
};

Object.assign(window, addAllOfThisToWindow);

declare global {
  type StuffToAdd = typeof addAllOfThisToWindow;
  interface Window extends StuffToAdd {}
}

// Now everything is merged into Window.
// This pattern is really useful for things like assigning environment 
// variables to certain defaults, or any other situations where you want 
// to stick things in the global scope.

Type-Checking React Props with discriminated unions to create flexible props

type ModalProps =
  | {
      variant: "no-title";
    }
  | {
      variant: "title";
      title: string;
    };

export const Modal = (props: ModalProps) => {
  if (props.variant === "no-title") {
    return <div>No title</div>
  } else {
    return <div>Title: {props.title}</div>
  }
}

Destructuring vs accessing discriminated union props in React

type ModalProps =
  | {
    variant: "no-title"
    }
  | {
    variant: "title"
      title: string
    }

export const Modal = (props: ModalProps) => {
  if (props.variant === "no-title") {
    return <div>No title</div>
  } else {
    return <div>Title: {title}</div>
  }
}

// Then TypeScript will understand that the thing we're narrowing 
// is props. After the variant has been narrowed, we can destructure 
// the title from props, though generally I would prefer just to access 
// it off of props

Discriminated unions for conditional props in TypeScript

// Solution #1
type EmbeddedPlaygroundProps =
  | {
      useStackBlitz: true
      stackBlitzId: string
    }
  | {
      useStackBlitz: false
      codeSandboxId: string
    }
  | {
      useStackBlitz?: undefined
      codeSandboxId: string
    }

// Solution #2 (better)
// When useStackBlitz is true, a stackBlitzId is required.
// When it's false or not passed in at all (i.e., undefined), 
// a codeSandboxId is required.
type EmbeddedPlaygroundProps =
  | {
      useStackBlitz: true
      stackBlitzId: string
    }
  | {
      useStackBlitz?: false
      codeSandboxId: string
    }

Representing an empty object

// Right way
// If we want to represent config as an object without any properties, 
// ensuring an object is being passed, then you would pass in a 
// Record<string, never>. We have to pass in an empty object. If we try 
// to pass in anything else, it will trigger an error because foo is 
// not assignable to type never.
const Component = (props: { config: Record<string, never> }) => {
  return <div />
}

// This type is somewhat unusual because it allows us to pass virtually 
// anything we want into config with a few restrictions. The only 
// circumstances that will trigger an error are when we pass null or
// undefined.
const Component = (props: { config: {} }) => {
  return <div />
}

Conditionally require props with discriminated unions

type InputProps = (
  | {
      value: string
      onChange: ChangeEventHandler
    }
  | {
      value?: undefined
      onChange?: undefined
    }
) & {
  label: string
}

// A Less Desirable Solution
// This does work, but only because never in this situation gets 
// reduced to undefined.
type InputProps = (
  | {
      value: string
      onChange: ChangeEventHandler
    }
  | {
      value?: never
      onChange?: never
    }
) & {
  label: string
}

Partial autocompletion quirk

const presetSizes = {
  xs: "0.5rem",
  sm: "1rem",
};

type Size = keyof typeof presetSizes;

// Solution
type LooseSize = Size | (string & {});

// If we remove this intersection you'll notice that Size is just 
// inferred as a string (this might be fixed in newer TS versions). I think that somewhere in the compiler it's saying, "Let's eagerly compute this 
// before we even show it to the developers so that we get the autocomplete options."
// Adding the intersection delays the computation of the string, which means it's just in time to show us 
// the autocomplete for sm or xs is available.

Using as const and indexed access types to extract keys and values from a Type

const BACKEND_TO_FRONTEND_STATUS_MAP = {
  0: "pending",
  1: "success",
  2: "error",
} as const;

// type BackendStatusMap = {
//   readonly 0: "pending";
//   readonly 1: "success";
//   readonly 2: "error";
// }
type BackendStatusMap = typeof BACKEND_TO_FRONTEND_STATUS_MAP;

// type BackendStatus = 0 | 1 | 2
type BackendStatus = keyof BackendStatusMap;

// type FrontendStatus = "pending" | "success" | "error"
type FrontendStatus = BackendStatusMap[BackendStatus];

Inference from a single source of truth. Implementing dynamic props mapping in React

/**
 * satisfies gives us what we need. Now, we get autocomplete inside
 * buttonPropsMap, and we get type safety on the variant in ButtonProps.
 *
 * That's because satisfies ensures buttonPropsMap is a Record, but 
 * doesn't override its type.
 */
const buttonPropsMap = {
  reset: {
    className: "bg-blue-500 text-white",
    type: "reset",
    // @ts-expect-error
    illegalProperty: "whatever",
  },
  submit: {
    className: "bg-gray-200 text-black",
    type: "submit",
    // @ts-expect-error
    illegalProperty: "whatever",
  },
  next: {
    className: "bg-green-500 text-white",
    type: "button",
    // @ts-expect-error
    illegalProperty: "whatever",
  },
} satisfies Record<string, ComponentProps<"button">>;

type ButtonProps = {
  variant: keyof typeof buttonPropsMap;
};

export const Button = (props: ButtonProps) => {
  return <button {...buttonPropsMap[props.variant]}>Click me</button>;
};

Add a generic type argument to a props Interface in a React component

interface TableProps<T> {
  rows: T[];
  renderRow: (row: T) => ReactNode;
}

//**** <T,> or <T extends unknown>
export const Table = <T,>(props: TableProps<T>) => {
  return (
    <table>
      <tbody>
        {props.rows.map((row) => (
          <tr>{props.renderRow(row)}</tr>
        ))}
      </tbody>
    </table>
  );
};

// Note that because we are in a .tsx file, we need to add a comma 
// at the end of the type argument to let TypeScript know that this 
// is not a JSX element.

Use the angle brackets syntax to pass a type to a component

<Table<User>
    // @ts-expect-error rows should be User[]
    rows={[1, 2, 3]}
    renderRow={(row) => {
      type test = Expect<Equal<typeof row, User>>;
      return <td>{row.name}</td>;
    }}
  />

Fixing type inference in a custom react hook

// Use as const on the returned array:
export const useId = (defaultId: string) => {
  const [id, setId] = useState(defaultId);

  return [id, setId] as const;
};

Strongly typing React Context (using type arguments to create a strongly typed Context)

const createRequiredContext = <T,>() => {
  const context = React.createContext<T | null>(null);

  const useContext = () => {
    const contextValue = React.useContext(context);

    if (contextValue === null) {
      throw new Error("Context value is null");
    }

    return contextValue;
  };

  return [useContext, context.Provider] as const;
};

const [useUser, UserProvider] = createRequiredContext<{
  name: string;
}>();

Improved type safety with discriminated tuples in TypeScript

export type Result<T> =
    | ["loading", undefined?]
    | ["success", T]
    | ["error", Error];

// inside of useData
const [result, setResult] = useState<Result<T>>([""]);
// autocomplete for error, loading, or success

Strongly typing lazy loaded React components with generics

import { ComponentProps, ComponentType, lazy, Suspense, useMemo } from "react";

type Props<C extends ComponentType<any>> = {
  loader: () => Promise<{
    default: C;
  }>;
} & ComponentProps<C>;

function LazyLoad<C extends ComponentType<any>>({
  loader,
  ...props
}: Props<C>) {
  const LazyComponent = useMemo(() => lazy(loader), [loader]);

  return (
    <Suspense fallback={"Loading..."}>
      <LazyComponent {...props} />
    </Suspense>
  );
}

<>
  <LazyLoad loader={() => import("fake-external-component")} id="123" />

  <LazyLoad
    loader={() => import("fake-external-component")}
    // @ts-expect-error number is not assignable to string
    id={123}
  />

  {/* @ts-expect-error id is missing! */}
  <LazyLoad loader={() => import("fake-external-component")} />
</>;

Infer shared props for multiple React components with React.ComponentProps and satisfies

type InputProps = React.ComponentProps<"input">;

/**
 * OR, we can do it by making COMPONENTS 'satisfy'
 * a type that is a Record of React.FC<InputProps>.
 */
const COMPONENTS = {
  text: (props) => {
    return <input {...props} type="text" />;
  },
  number: (props) => {
    return <input {...props} type="number" />;
  },
  password: (props) => {
    return <input {...props} type="password" />;
  },
} satisfies Record<string, React.ComponentType<InputProps>>;

/**
 * Then, we can derive the type of input from the
 * keys of COMPONENTS.
 */
type Input = keyof typeof COMPONENTS;

export const Input = (props: { type: Input } & InputProps) => {
  const Component = COMPONENTS[props.type];
  return <Component {...props} />;
};

The problem with forwardRef in React

// Just use this in your projects 
declare module "react" {
    function forwardRef<T, P = {}>(
        render: (props: P, ref: React.Ref<T>) => React.ReactNode
    ): (props: P & React.RefAttributes<T>) => React.ReactNode;
}