[LS] TypeScript Concepts + Examples
Table of contents
- Extract a promise's result
- Extract function parameters
- Create unions from objects using keyof
- Discriminated union types
- Extract from a union using a utility type
- Remove a single member of a union
- Use indexed access types to extract object properties
- Infer object's values as Literal Types with as const
- Extract specific members from a union with indexed access
- Get all the values of an object with Obj[keyof Obj]
- Creating a union out of every element of an array
- Template literal with strings
- Extracting string pattern matches with template literals
- Passing unions into template literals
- Use a utility type to create an object from a union
- Manipulate string literals using type helpers
- Use constraints with extends to limit type parameters
- Support function type constraints with variable arguments
- Exclude null and undefined from the Maybe type
- Enforce a minimum array length in a type helper
- Compare and return values with extends and the ternary operator
- Infer elements inside a conditional with infer
- Extract type arguments to another type helper
- Pattern matching on template literals with infer
- Extract the result of an async function
- Extract the result from several possible function shapes
- Map οver a union to create an object
- Mapped types with objects
- Transforming object keys in mapped types
- Conditionally extract properties from an object
- Map a discriminated union to an object
- Map an object to a union of tuples
- Transform an object into a union of template literals
- Transform a discriminated union into a union
- Transform path parameters from strings to objects
- Transform an object into a discriminated union
- Transform a discriminated union with unique values to an object
- Construct a deep partial of an object
- Add object property constraints to a generic function
- Use generics to type a fetch request
- Represent generics at the lowest level
- Typed object keys
- Inferring literal types from any basic type
- Infer the type of an array member
- Ensure runtime level & type level safety with conditional types
- Function overloads vs. conditional types
- Specifying types for an overloaded function
- Create a function with a dynamic number of arguments
- Modify a generic type default for improved error messages
- Use declaration merging to add functionality to the global window
- Typing process.env in the nodeJS Namespace
- Use a type predicate to filter types
- Using const type parameters for better inference
- Add objects to the global scope dynamically
- Type-Checking React Props with discriminated unions to create flexible props
- Destructuring vs accessing discriminated union props in React
- Discriminated unions for conditional props in TypeScript
- Representing an empty object
- Conditionally require props with discriminated unions
- Partial autocompletion quirk
- Using as const and indexed access types to extract keys and values from a Type
- Inference from a single source of truth. Implementing dynamic props mapping in React
- Add a generic type argument to a props Interface in a React component
- Use the angle brackets syntax to pass a type to a component
- Fixing type inference in a custom react hook
- Strongly typing React Context (using type arguments to create a strongly typed Context)
- Improved type safety with discriminated tuples in TypeScript
- Strongly typing lazy loaded React components with generics
- Infer shared props for multiple React components with React.ComponentProps and satisfies
- The problem with forwardRef in React
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;
}