Last active
November 29, 2025 10:40
-
-
Save TCotton/ce390fda3eaae533fc3780876669862c to your computer and use it in GitHub Desktop.
TypeScript cheatsheet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // return types from functions | |
| type P = ReturnType<typeof dateFromUUIDv7>; | |
| // return type with promise | |
| type P = Awaited<ReturnType<typeof dateFromUUIDv7>>; | |
| // see all types | |
| type PrettifyPromise<T> = { | |
| [K in keyof T]: T[K] extends Promise<infer U> ? U : T[K]; | |
| } & {} | |
| type Prettify<T> = { | |
| [K in keyof T]: T[K] | |
| } & {} | |
| type idk = Prettify<DateFromUUIDv7Result> | |
| // safe typescrip way of creating unknown Record | |
| export type UnknownRecord = Record<PropertyKey, unknown>; | |
| //This type signature Buffer & { length: 16 } is too strict for practical use. | |
| // While it might seem like a good way to enforce 16-byte buffers at compile-time, | |
| // TypeScript's type system cannot verify at compile-time that a dynamically | |
| // created Buffer will have exactly 16 bytes. This type only works if you explicitly | |
| // type-assert every buffer as having length: 16, which defeats the purpose of type safety. | |
| // note on static check | |
| // this is permissable but unadvisabe | |
| type UuidBuffer = Buffer & { length: 5 }; | |
| // It creates a refined (narrower) type by taking the existing Buffer type | |
| // and intersecting it with an additional constraint that says length must be exactly 16. | |
| // However, this is NOT | |
| type StringCheck = String & { length: 5 }; | |
| // TypeScript does not expose a length property as a literal type on primitive string, | |
| // while it does on tuples and on certain narrowed arrays. | |
| // So you can write it, but it won’t actually narrow at compile time. | |
| // This is the only way to check for string length. This checks for five characters | |
| // This checks for a fixed length array | |
| type FixedLengthArray<T, N extends number> = N extends N ? number extends N ? Array<T> : _FixedLengthArray<T, N, []> | |
| : never | |
| type _FixedLengthArray<T, N extends number, R extends Array<unknown>> = R['length'] extends N ? R | |
| : _FixedLengthArray<T, N, [T, ...R]> | |
| type Array16<T> = FixedLengthArray<T, 16> | |
| type Hash256Bytes = Array16<number> | |
| const anArray = [1, 2, 3] | |
| // for the same reasons above, this permissible but unadvisable | |
| function takesHash256Bytes(input: Hash256Bytes = anArray): Hash256Bytes { | |
| return input | |
| } | |
| // Pair the above TS with a runtime type guard: | |
| function isArray16<T>(arr: Array<T>): arr is Array16<T> { | |
| return arr.length === 16 | |
| } | |
| // Using Generic Context to Avoid Distributive Conditional Types | |
| type Fruit = "apple" | "banana" | "orange"; | |
| type GetAppleOrBanana<T> = T extends "apple" | "banana" ? T : never | |
| type AppleOrBanana = GetAppleOrBanana<Fruit> | |
| type tests = [Expect<Equal<AppleOrBanana, "apple" | "banana">>]; | |
| // safe typescrip way of creating unknown Record | |
| export type UnknownRecord = Record<PropertyKey, unknown>; | |
| // type narrowing with Record<PropertyKey, unknown> | |
| // Source - https://stackoverflow.com/a/79489937 | |
| // Posted by jcalz | |
| // Retrieved 2025-11-15, License - CC BY-SA 4.0 | |
| const parseAsRecord = (variable: unknown): Record<PropertyKey, unknown> => { | |
| if ( | |
| typeof variable !== "object" || | |
| Array.isArray(variable) || | |
| variable === null | |
| ) { | |
| throw new Error("Not Record"); | |
| } | |
| return variable as Record<PropertyKey, unknown> | |
| }; | |
| const parseAsRecord = (variable: Record<PropertyKey, unknown> | string | number | boolean | bigint | null | undefined | unknown[]) => { | |
| if ( | |
| typeof variable !== "object" || | |
| Array.isArray(variable) || | |
| variable === null | |
| ) { | |
| throw new Error("Not Record"); | |
| } | |
| return variable; | |
| }; | |
| parseAsRecord('string'); | |
| parseAsRecord(null); | |
| parseAsRecord({}); | |
| parseAsRecord([]) | |
| const testingFrameworks = { | |
| vitest: { | |
| label: "Vitest", | |
| }, | |
| jest: { | |
| label: "Jest", | |
| }, | |
| mocha: { | |
| label: "Mocha", | |
| }, | |
| }; | |
| // Create Unions from Objects Using Two Operators | |
| type k = keyof typeof testingFrameworks; | |
| // take parameters from function | |
| const makeQuery = ( | |
| url: string, | |
| opts?: { | |
| method?: string; | |
| headers?: { | |
| [key: string]: string; | |
| }; | |
| body?: string; | |
| } | |
| ) => {}; | |
| type MakeQueryParameters = Parameters<typeof makeQuery>; | |
| export type Event = | |
| | { | |
| type: "click"; | |
| event: MouseEvent; | |
| } | |
| | { | |
| type: "focus"; | |
| event: FocusEvent; | |
| } | |
| | { | |
| type: "keydown"; | |
| event: KeyboardEvent; | |
| }; | |
| // Extract From A Union Using a Utility Type | |
| type ClickEvent = Extract<Event, { type: "click" }> | |
| export type Event = | |
| | { | |
| type: "click"; | |
| event: MouseEvent; | |
| } | |
| | { | |
| type: "focus"; | |
| event: FocusEvent; | |
| } | |
| | { | |
| type: "keydown"; | |
| event: KeyboardEvent; | |
| }; | |
| // Use a Utility Type to Remove a Single Member of a Union | |
| type NonKeyDownEvents = Exclude<Event, { type: "keydown" }> | |
| export const fakeDataDefaults = { | |
| String: "Default string", | |
| Int: 1, | |
| Float: 1.14, | |
| Boolean: true, | |
| ID: "id", | |
| }; | |
| // Use Indexed Access Types to Extract Object Properties | |
| type FakeDataDefaults = typeof fakeDataDefaults | |
| export type StringType = FakeDataDefaults["String"] | |
| export type IntType = FakeDataDefaults["Int"] | |
| export type FloatType = FakeDataDefaults["Float"] | |
| export type BooleanType = FakeDataDefaults["Boolean"] | |
| export type IDType = FakeDataDefaults["ID"] | |
| // Simple Syntax Used to Access Parts of a Discriminated Union | |
| type EventType = Event["type"]; | |
| export const programModeEnumMap = { | |
| GROUP: "group", | |
| ANNOUNCEMENT: "announcement", | |
| ONE_ON_ONE: "1on1", | |
| SELF_DIRECTED: "selfDirected", | |
| PLANNED_ONE_ON_ONE: "planned1on1", | |
| PLANNED_SELF_DIRECTED: "plannedSelfDirected", | |
| } as const; | |
| export type IndividualProgram = typeof programModeEnumMap[ | |
| | "ONE_ON_ONE" | |
| | "SELF_DIRECTED" | |
| | "PLANNED_ONE_ON_ONE" | |
| | "PLANNED_SELF_DIRECTED" | |
| ] | |
| export type IndividualProgram = typeof programModeEnumMap[ | |
| Exclude< | |
| keyof typeof programModeEnumMap, | |
| "GROUP" | "ANNOUNCEMENT" | |
| > | |
| ] | |
| const frontendToBackendEnumMap = { | |
| singleModule: "SINGLE_MODULE", | |
| multiModule: "MULTI_MODULE", | |
| sharedModule: "SHARED_MODULE", | |
| } as const; | |
| // Use Two Operators With Indexed Access to Get All of an Object's Values | |
| type BackendModuleEnum = | |
| typeof frontendToBackendEnumMap[keyof typeof frontendToBackendEnumMap] | |
| const fruits = ["apple", "banana", "orange"] as const; | |
| // Methods Used to Create Unions out of Array Values | |
| type AppleOrBanana = typeof fruits[0 | 1]; | |
| type Fruit = typeof fruits[number]; | |
| // use template literals | |
| type Route = `/${string}` | |
| export const goToRoute = (route: Route) => {}; | |
| // Extracting String Pattern Matches with Template Literals | |
| type Routes = "/users" | "/users/:id" | "/posts" | "/posts/:id"; | |
| type DynamicRoutes = Extract<Routes, `${string}:${string}`> | |
| // Passing Unions Into Template Literals | |
| type BreadType = "rye" | "brown" | "white"; | |
| type Filling = "cheese" | "ham" | "salami"; | |
| type Sandwich = `${BreadType} sandwich with ${Filling}`; | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| Sandwich, | |
| | "rye sandwich with cheese" | |
| | "rye sandwich with ham" | |
| | "rye sandwich with salami" | |
| | "brown sandwich with cheese" | |
| | "brown sandwich with ham" | |
| | "brown sandwich with salami" | |
| | "white sandwich with cheese" | |
| | "white sandwich with ham" | |
| | "white sandwich with salami" | |
| > | |
| > | |
| ]; | |
| // Manipulate String Literals Using Type Helpers - to uppercase | |
| type Event = `log_in` | "log_out" | "sign_up"; | |
| type ObjectOfKeys = Record<Uppercase<Event>, string>; | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| ObjectOfKeys, | |
| { | |
| LOG_IN: string; | |
| LOG_OUT: string; | |
| SIGN_UP: string; | |
| } | |
| > | |
| > | |
| // Use Constraints to Limit Type Parameters | |
| type AddRoutePrefix<TRoute extends string> = `/${TRoute}`; | |
| type tests = [ | |
| Expect<Equal<AddRoutePrefix<"">, "/">>, | |
| Expect<Equal<AddRoutePrefix<"about">, "/about">>, | |
| Expect<Equal<AddRoutePrefix<"about/team">, "/about/team">>, | |
| Expect<Equal<AddRoutePrefix<"blog">, "/blog">>, | |
| // @ts-expect-error | |
| AddRoutePrefix<boolean>, | |
| // @ts-expect-error | |
| AddRoutePrefix<number>, | |
| ]; | |
| // Add Support for Multiple Types in a Type Helper | |
| type CreateDataShape = { | |
| data: unknown; | |
| error: unknown; | |
| }; | |
| type CreateDataShape<TData, TError> = { | |
| data: TData; | |
| error: TError; | |
| }; | |
| //S et a Default Type Value in a Type Helper | |
| type CreateDataShape<TData, TError extends Error = undefined> | |
| data: TData; | |
| error: TError; | |
| }; | |
| // Support Function Type Constraints with Variable Arguments | |
| type GetParametersAndReturnType<T extends () => any> = {} | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| GetParametersAndReturnType<() => string>, | |
| { params: []; returnValue: string } | |
| > | |
| >, | |
| Expect< | |
| Equal< | |
| GetParametersAndReturnType<(s: string) => void>, | |
| { params: [string]; returnValue: void } | |
| > | |
| >, | |
| Expect< | |
| Equal< | |
| GetParametersAndReturnType<(n: number, b: boolean) => number>, | |
| { params: [number, boolean]; returnValue: number } | |
| > | |
| >, | |
| ]; | |
| // Exclude null and undefined from the Maybe Type | |
| export type Maybe<T extends {}> = T | null | undefined; | |
| // Compare and Return Values with Extends and the Ternary Operator | |
| type YouSayGoodbyeAndISayHello<T> = T extends "hello" ? "goodbye" : "hello"; | |
| // Infer Elements Inside a Conditional with Infer | |
| type GetDataValue<T> = T extends { data: infer TData } | |
| ? TData | |
| : never; | |
| type tests = [ | |
| Expect<Equal<GetDataValue<{ data: "hello" }>, "hello">>, | |
| Expect<Equal<GetDataValue<{ data: { name: "hello" } }>, { name: "hello" }>>, | |
| Expect< | |
| Equal< | |
| GetDataValue<{ data: { name: "hello"; age: 20 } }>, | |
| { name: "hello"; age: 20 } | |
| > | |
| >, | |
| // Expect that if you pass in string, it | |
| // should return never | |
| Expect<Equal<GetDataValue<string>, never>>, | |
| ]; | |
| // Use infer with Generics to Extract Types from Arguments | |
| interface MyComplexInterface<Event, Context, Name, Point> { | |
| getEvent: () => Event; | |
| getContext: () => Context; | |
| getName: () => Name; | |
| getPoint: () => Point; | |
| } | |
| type Example = MyComplexInterface< | |
| "click", | |
| "window", | |
| "my-event", | |
| { x: 12; y: 14 } | |
| >; | |
| type GetPoint<T> = T extends MyComplexInterface<any, any, any, any> | |
| ? any | |
| : never; | |
| type tests = [Expect<Equal<GetPoint<Example>, { x: 12; y: 14 }>>]; | |
| // Pattern Matching on Template Literals with Infer | |
| type Names = [ | |
| "Matt Pocock", | |
| "Jimi Hendrix", | |
| "Eric Clapton", | |
| "John Mayer", | |
| "BB King", | |
| ]; | |
| type GetSurname<T> = T extends `${infer First} ${infer Last}` ? Last : never | |
| type tests = [ | |
| Expect<Equal<GetSurname<Names[0]>, "Pocock">>, | |
| Expect<Equal<GetSurname<Names[1]>, "Hendrix">>, | |
| Expect<Equal<GetSurname<Names[2]>, "Clapton">>, | |
| Expect<Equal<GetSurname<Names[3]>, "Mayer">>, | |
| Expect<Equal<GetSurname<Names[4]>, "King">>, | |
| ]; | |
| // Optionally Infer the Return Type of a 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 | |
| ; | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| InferPropsFromServerSideFunction<typeof getServerSideProps>, | |
| { json: { title: string } } | |
| > | |
| > | |
| ]; | |
| // Extract the Result From Several Possible Function Shapes | |
| const parser1 = { | |
| parse: () => 1, | |
| }; | |
| const parser2 = () => "123"; | |
| const parser3 = { | |
| extract: () => true, | |
| }; | |
| type GetParserResult<T> = T extends | |
| | { | |
| parse: () => infer TResult | |
| } | |
| | { | |
| extract: () => infer TResult | |
| } | |
| | (() => infer TResult) | |
| ? TResult | |
| : never | |
| type tests = [ | |
| Expect<Equal<GetParserResult<typeof parser1>, number>>, | |
| Expect<Equal<GetParserResult<typeof parser2>, string>>, | |
| Expect<Equal<GetParserResult<typeof parser3>, boolean>>, | |
| ]; | |
| // Use Mapped Types to Create an Object from a Union | |
| type Route = "/" | "/about" | "/admin" | "/admin/users"; | |
| type RoutesObject = { | |
| [R in Route]: R | |
| } | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| RoutesObject, | |
| { | |
| "/": "/"; | |
| "/about": "/about"; | |
| "/admin": "/admin"; | |
| "/admin/users": "/admin/users"; | |
| } | |
| > | |
| >, | |
| ]; | |
| // Mapped Types with Objects | |
| interface Attributes { | |
| firstName: string; | |
| lastName: string; | |
| age: number; | |
| } | |
| type AttributeGetters = { | |
| [K in keyof Attributes]: () => Attributes[K] | |
| } | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| AttributeGetters, | |
| { | |
| firstName: () => string; | |
| lastName: () => string; | |
| age: () => number; | |
| } | |
| > | |
| >, | |
| ]; | |
| // Transforming Object Keys in Mapped Types | |
| interface Attributes { | |
| firstName: string; | |
| lastName: string; | |
| age: number; | |
| } | |
| type AttributeGetters = { | |
| [K in keyof Attributes as `get${Capitalize<K>}`]: () => Attributes[K] | |
| } | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| AttributeGetters, | |
| { | |
| getFirstName: () => string; | |
| getLastName: () => string; | |
| getAge: () => number; | |
| } | |
| > | |
| > | |
| ]; | |
| // Conditionally Extract Properties from Object | |
| interface Example { | |
| name: string; | |
| age: number; | |
| id: string; | |
| organisationId: string; | |
| groupId: string; | |
| } | |
| type SearchForId = `${string}${"id" | "Id"}${string}` | |
| type OnlyIdKeys<T> = { | |
| [K in keyof T as K extends SearchForId ? K : never]: T[K] | |
| } | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| OnlyIdKeys<Example>, | |
| { | |
| id: string; | |
| organisationId: string; | |
| groupId: string; | |
| } | |
| > | |
| >, | |
| Expect<Equal<OnlyIdKeys<{}>, {}>> | |
| ]; | |
| // Map a Discriminated Union to an Object | |
| type Route = | |
| | { | |
| route: "/"; | |
| search: { | |
| page: string; | |
| perPage: string; | |
| }; | |
| } | |
| | { route: "/about"; search: {} } | |
| | { route: "/admin"; search: {} } | |
| | { route: "/admin/users"; search: {} }; | |
| type RoutesObject = { | |
| [R in Route as R["route"]]: R["search"] | |
| } | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| RoutesObject, | |
| { | |
| "/": { | |
| page: string; | |
| perPage: string; | |
| }; | |
| "/about": {}; | |
| "/admin": {}; | |
| "/admin/users": {}; | |
| } | |
| > | |
| >, | |
| ]; | |
| // Map an Object to a Union of Tuples | |
| interface Values { | |
| email: string; | |
| firstName: string; | |
| lastName: string; | |
| } | |
| type ValuesAsUnionOfTuples = { | |
| [K in keyof Values]: [K, Values[K]] | |
| }[keyof Values] | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| 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] | |
| type tests = [ | |
| Expect< | |
| Equal<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"]] | |
| type tests = [ | |
| Expect< | |
| Equal<TransformedFruit, "apple:red" | "banana:yellow" | "orange:orange"> | |
| >, | |
| ]; | |
| // Transform an Object into a Discriminated Union | |
| interface Attributes { | |
| id: string; | |
| email: string; | |
| username: string; | |
| } | |
| /** | |
| * How do we create a type helper that represents a union | |
| * of all possible combinations of Attributes? | |
| */ | |
| type MutuallyExclusive<T> = { | |
| [K in keyof T]: Record<K, T[K]>; | |
| }[keyof T]; | |
| type ExclusiveAttributes = MutuallyExclusive<Attributes>; | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| ExclusiveAttributes, | |
| | { | |
| id: string; | |
| } | |
| | { | |
| email: string; | |
| } | |
| | { | |
| username: string; | |
| } | |
| > | |
| >, | |
| ]; | |
| // 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" }; | |
| type RoutesObject = { | |
| [R in Route as R["route"]]: R extends { search: infer S } ? S : never; | |
| }; | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| RoutesObject, | |
| { | |
| "/": { | |
| page: string; | |
| perPage: string; | |
| }; | |
| "/about": never; | |
| "/admin": never; | |
| "/admin/users": never; | |
| } | |
| > | |
| >, | |
| ]; | |
| // Construct a Deep Partial of an Object | |
| type DeepPartial<T> = T extends Array<infer U> | |
| ? Array<DeepPartial<U>> | |
| : { [K in keyof T]?: DeepPartial<T[K]> }; | |
| type MyType = { | |
| a: string; | |
| b: number; | |
| c: { | |
| d: string; | |
| e: { | |
| f: string; | |
| g: { | |
| h: string; | |
| i: string; | |
| }[]; | |
| }; | |
| }; | |
| }; | |
| type Result = DeepPartial<MyType>; | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| Result, | |
| { | |
| a?: string; | |
| b?: number; | |
| c?: { | |
| d?: string; | |
| e?: { | |
| f?: string; | |
| g?: { | |
| h?: string; | |
| i?: string; | |
| }[]; | |
| }; | |
| }; | |
| } | |
| > | |
| > | |
| ]; | |
| // Generics | |
| // Typing Functions with Generics | |
| const returnWhatIPassIn = <T>(t: T) => { | |
| return t; | |
| }; | |
| const one = returnWhatIPassIn(1); | |
| const matt = returnWhatIPassIn("matt"); | |
| type tests = [Expect<Equal<typeof one, 1>>, Expect<Equal<typeof matt, "matt">>]; | |
| // Restricting Generic Argument Types | |
| export const returnWhatIPassIn = <T extends string>(t: T) => t; | |
| it("Should ONLY allow strings to be passed in", () => { | |
| const a = returnWhatIPassIn("a"); | |
| type test1 = Expect<Equal<typeof a, "a">>; | |
| // @ts-expect-error | |
| returnWhatIPassIn(1); | |
| // @ts-expect-error | |
| returnWhatIPassIn(true); | |
| // @ts-expect-error | |
| returnWhatIPassIn({ | |
| foo: "bar", | |
| }); | |
| }); | |
| // Typing Independent Parameters | |
| const returnBothOfWhatIPassIn = <T, K>(a: T, b: K) => { | |
| return { | |
| a, | |
| b, | |
| }; | |
| }; | |
| it("Should return an object of the arguments you pass", () => { | |
| const result = returnBothOfWhatIPassIn("a", 1); | |
| expect(result).toEqual({ | |
| a: "a", | |
| b: 1, | |
| }); | |
| type test1 = Expect< | |
| Equal< | |
| typeof result, | |
| { | |
| a: string; | |
| b: number; | |
| } | |
| > | |
| >; | |
| }); | |
| // Approaches for Typing Object Parameters | |
| const returnBothOfWhatIPassIn = <T, K>(params: { a: T; b: K }) => { | |
| return { | |
| first: params.a, | |
| second: params.b, | |
| }; | |
| }; | |
| it("Should return an object where a -> first and b -> second", () => { | |
| const result = returnBothOfWhatIPassIn({ | |
| a: "a", | |
| b: 1, | |
| }); | |
| expect(result).toEqual({ | |
| first: "a", | |
| second: 1, | |
| }); | |
| type test1 = Expect< | |
| Equal< | |
| typeof result, | |
| { | |
| first: string; | |
| second: number; | |
| } | |
| > | |
| >; | |
| }); | |
| // Generics in Classes | |
| export class Component<T> { | |
| private props: T; | |
| constructor(props: T) { | |
| this.props = props; | |
| } | |
| getProps = () => this.props; | |
| } | |
| it("Should create an object containing props", () => { | |
| const component = new Component({ a: 1, b: 2, c: 3 }); | |
| const result = component.getProps(); | |
| expect(result).toEqual({ a: 1, b: 2, c: 3 }); | |
| type tests = [ | |
| Expect<Equal<typeof result, { a: number; b: number; c: number }>>, | |
| ]; | |
| }); | |
| // Generic Mapper Function | |
| export const concatenateFirstNameAndLastName = < | |
| TUser extends { firstName: string; lastName: string }, | |
| >( | |
| user: TUser, | |
| ) => { | |
| return { | |
| ...user, | |
| fullName: `${user.firstName} ${user.lastName}`, | |
| }; | |
| }; | |
| it("Should add fullName to an object which only contains firstName and lastName", () => { | |
| const users = [ | |
| { | |
| firstName: "Matt", | |
| lastName: "Pocock", | |
| }, | |
| ]; | |
| const newUsers = users.map(concatenateFirstNameAndLastName); | |
| expect(newUsers).toEqual([ | |
| { | |
| firstName: "Matt", | |
| lastName: "Pocock", | |
| fullName: "Matt Pocock", | |
| }, | |
| ]); | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| typeof newUsers, | |
| Array<{ firstName: string; lastName: string } & { fullName: string }> | |
| > | |
| >, | |
| ]; | |
| }); | |
| it("Should retain other properties passed in", () => { | |
| const users = [ | |
| { | |
| id: 1, | |
| firstName: "Matt", | |
| lastName: "Pocock", | |
| }, | |
| ]; | |
| const newUsers = users.map(concatenateFirstNameAndLastName); | |
| expect(newUsers).toEqual([ | |
| { | |
| id: 1, | |
| firstName: "Matt", | |
| lastName: "Pocock", | |
| fullName: "Matt Pocock", | |
| }, | |
| ]); | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| typeof newUsers, | |
| Array< | |
| { id: number; firstName: string; lastName: string } & { | |
| fullName: string; | |
| } | |
| > | |
| > | |
| >, | |
| ]; | |
| }); | |
| it("Should fail when the object passed in does not contain firstName", () => { | |
| const users = [ | |
| { | |
| firstName: "Matt", | |
| }, | |
| ]; | |
| const newUsers = users.map( | |
| // @ts-expect-error | |
| concatenateFirstNameAndLastName, | |
| ); | |
| }); | |
| // Pass Type Arguments to a Reduce Function | |
| const array = [ | |
| { | |
| name: "John", | |
| }, | |
| { | |
| name: "Steve", | |
| }, | |
| ]; | |
| const obj = array.reduce<Record<string, { name: string }>>((accum, item) => { | |
| accum[item.name] = item; | |
| return accum; | |
| }, {}); | |
| it("Should resolve to an object where name is the key", () => { | |
| expect(obj).toEqual({ | |
| John: { | |
| name: "John", | |
| }, | |
| Steve: { | |
| name: "Steve", | |
| }, | |
| }); | |
| type tests = [Expect<Equal<typeof obj, Record<string, { name: string }>>>]; | |
| }); | |
| // Avoid any Types with Generics | |
| const fetchData = async <TData>(url: string) => { | |
| const data: TData = await fetch(url).then((response) => response.json()); | |
| return data; | |
| }; | |
| it("Should fetch data from an API", async () => { | |
| const data = await fetchData<{ name: string }>( | |
| "https://swapi.py4e.com/api/people/1", | |
| ); | |
| expect(data.name).toEqual("Luke Skywalker"); | |
| type tests = [Expect<Equal<typeof data, { name: string }>>]; | |
| }); | |
| // Generics at Different Levels | |
| export const getHomePageFeatureFlags = <HomePageFlags>( | |
| config: { | |
| rawConfig: { | |
| featureFlags: { | |
| homePage: HomePageFlags; | |
| }; | |
| }; | |
| }, | |
| override: (flags: HomePageFlags) => HomePageFlags | |
| ) => { | |
| return override(config.rawConfig.featureFlags.homePage); | |
| }; | |
| describe("getHomePageFeatureFlags", () => { | |
| const EXAMPLE_CONFIG = { | |
| apiEndpoint: "https://api.example.com", | |
| apiVersion: "v1", | |
| apiKey: "1234567890", | |
| rawConfig: { | |
| featureFlags: { | |
| homePage: { | |
| showBanner: true, | |
| showLogOut: false, | |
| }, | |
| loginPage: { | |
| showCaptcha: true, | |
| showConfirmPassword: false, | |
| }, | |
| }, | |
| }, | |
| }; | |
| it("Should return the homePage flag object", () => { | |
| const flags = getHomePageFeatureFlags( | |
| EXAMPLE_CONFIG, | |
| (defaultFlags) => defaultFlags | |
| ); | |
| expect(flags).toEqual({ | |
| showBanner: true, | |
| showLogOut: false, | |
| }); | |
| type tests = [ | |
| Expect<Equal<typeof flags, { showBanner: boolean; showLogOut: boolean }>> | |
| ]; | |
| }); | |
| it("Should allow you to modify the result", () => { | |
| const flags = getHomePageFeatureFlags(EXAMPLE_CONFIG, (defaultFlags) => ({ | |
| ...defaultFlags, | |
| showBanner: false, | |
| })); | |
| expect(flags).toEqual({ | |
| showBanner: false, | |
| showLogOut: false, | |
| }); | |
| type tests = [ | |
| Expect<Equal<typeof flags, { showBanner: boolean; showLogOut: boolean }>> | |
| ]; | |
| }); | |
| }); | |
| // Two Approaches for Typing Object Keys | |
| const typedObjectKeys = <TKey extends string>(obj: Record<TKey, any>) => { | |
| return Object.keys(obj) as Array<TKey>; | |
| }; | |
| it("Should return the keys of the object", () => { | |
| const result1 = typedObjectKeys({ | |
| a: 1, | |
| b: 2, | |
| }); | |
| expect(result1).toEqual(["a", "b"]); | |
| type test = Expect<Equal<typeof result1, Array<"a" | "b">>>; | |
| }); | |
| // Constrain a Type Argument to a Function | |
| import { expect, it } from "vitest"; | |
| import { Equal, Expect } from "../helpers/type-utils"; | |
| const makeSafe = | |
| <TFunc extends (...args: any[]) => any>(func: TFunc) => | |
| ( | |
| ...args: Parameters<TFunc> | |
| ): | |
| | { | |
| type: "success"; | |
| result: ReturnType<TFunc>; | |
| } | |
| | { | |
| type: "failure"; | |
| error: Error; | |
| } => { | |
| try { | |
| const result = func(...args); | |
| return { | |
| type: "success", | |
| result, | |
| }; | |
| } catch (e) { | |
| return { | |
| type: "failure", | |
| error: e as Error, | |
| }; | |
| } | |
| }; | |
| (func: unknown) => | |
| ( | |
| ...args: unknown | |
| ): | |
| | { | |
| type: "success"; | |
| result: unknown; | |
| } | |
| | { | |
| type: "failure"; | |
| error: Error; | |
| } => { | |
| try { | |
| const result = func(...args); | |
| return { | |
| type: "success", | |
| result, | |
| }; | |
| } catch (e) { | |
| return { | |
| type: "failure", | |
| error: e as Error, | |
| }; | |
| } | |
| }; | |
| it("Should return the result with a { type: 'success' } on a successful call", () => { | |
| const func = makeSafe(() => 1); | |
| const result = func(); | |
| expect(result).toEqual({ | |
| type: "success", | |
| result: 1, | |
| }); | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| typeof result, | |
| | { | |
| type: "success"; | |
| result: number; | |
| } | |
| | { | |
| type: "failure"; | |
| error: Error; | |
| } | |
| > | |
| >, | |
| ]; | |
| }); | |
| it("Should return the error on a thrown call", () => { | |
| const func = makeSafe(() => { | |
| if (1 > 2) { | |
| return "123"; | |
| } | |
| throw new Error("Oh dear"); | |
| }); | |
| const result = func(); | |
| expect(result).toEqual({ | |
| type: "failure", | |
| error: new Error("Oh dear"), | |
| }); | |
| type tests = [ | |
| Expect< | |
| Equal< | |
| typeof result, | |
| | { | |
| type: "success"; | |
| result: string; | |
| } | |
| | { | |
| type: "failure"; | |
| error: Error; | |
| } | |
| > | |
| >, | |
| ]; | |
| }); | |
| it("Should properly match the function's arguments", () => { | |
| const func = makeSafe((a: number, b: string) => { | |
| return `${a} ${b}`; | |
| }); | |
| // @ts-expect-error | |
| func(); | |
| // @ts-expect-error | |
| func(1, 1); | |
| func(1, "1"); | |
| }); | |
| // Inferring Literal Types from any Basic Type | |
| export const inferItemLiteral = <T extends string | number>(t: T) => { | |
| return { | |
| output: t, | |
| } | |
| } | |
| const result1 = inferItemLiteral("a"); | |
| const result2 = inferItemLiteral(123); | |
| type tests = [ | |
| Expect<Equal<typeof result1, { output: "a" }>>, | |
| Expect<Equal<typeof result2, { output: 123 }>> | |
| ]; | |
| // @ts-expect-error | |
| const error1 = inferItemLiteral({ | |
| a: 1, | |
| }); | |
| // @ts-expect-error | |
| const error2 = inferItemLiteral([1, 2]); | |
| // Constrain to the Array Member, Not the Array | |
| const makeStatus = <TStatus extends string>(statuses: TStatus[]) => { | |
| return statuses; | |
| }; | |
| const statuses = makeStatus(["INFO", "DEBUG", "ERROR", "WARNING"]); | |
| type tests = [ | |
| Expect<Equal<typeof statuses, Array<"INFO" | "DEBUG" | "ERROR" | "WARNING">>>, | |
| ]; | |
| // Generics in a Class Names Creator | |
| const createClassNamesFactory = | |
| <TClasses extends Record<string, string>>(classes: TClasses) => | |
| (type: keyof TClasses, ...otherClasses: string[]) => { | |
| const classList = [classes[type], ...otherClasses]; | |
| return classList.join(" "); | |
| }; | |
| const getBg = createClassNamesFactory({ | |
| primary: "bg-blue-500", | |
| secondary: "bg-gray-500", | |
| }); | |
| it("Should let you create classes from a className factory", () => { | |
| expect(getBg("primary")).toEqual("bg-blue-500"); | |
| expect(getBg("secondary")).toEqual("bg-gray-500"); | |
| }); | |
| it("Should let you pass additional classes which get appended", () => { | |
| expect(getBg("primary", "text-white", "rounded", "p-4")).toEqual( | |
| "bg-blue-500 text-white rounded p-4" | |
| ); | |
| }); | |
| it("Should return a type of string", () => { | |
| const result = getBg("primary"); | |
| type test = Expect<Equal<typeof result, string>>; | |
| }); | |
| it("Should not let you pass invalid variants", () => { | |
| // @ts-expect-error | |
| getBg("123123"); | |
| }); | |
| it("Should not let you pass an invalid object to createClassNamesFactory", () => { | |
| // @ts-expect-error | |
| createClassNamesFactory([]); | |
| // @ts-expect-error | |
| createClassNamesFactory(123); | |
| createClassNamesFactory({ | |
| // @ts-expect-error | |
| a: 1, | |
| }); | |
| }); | |
| // Generics with Conditional Types | |
| function youSayGoodbyeISayHello<TGreeting extends "hello" | "goodbye">( | |
| greeting: TGreeting, | |
| ): TGreeting extends "hello" ? "goodbye" : "hello" { | |
| return (greeting === "goodbye" ? "hello" : "goodbye"); as any | |
| } | |
| it("Should return goodbye when hello is passed in", () => { | |
| const result = youSayGoodbyeISayHello("hello"); | |
| type test = [Expect<Equal<typeof result, "goodbye">>]; | |
| expect(result).toEqual("goodbye"); | |
| }); | |
| it("Should return hello when goodbye is passed in", () => { | |
| const result = youSayGoodbyeISayHello("goodbye"); | |
| type test = [Expect<Equal<typeof result, "hello">>]; | |
| expect(result).toEqual("hello"); | |
| }); | |
| // Fixing Errors in Generic Functions | |
| type Person = { | |
| name: string; | |
| age: number; | |
| birthdate: Date; | |
| }; | |
| export function remapPerson<Key extends keyof Person>( | |
| key: Key, | |
| value: Person[Key], | |
| ): Person[Key] { | |
| if (key === "birthdate") { | |
| return new Date() as Person[Key]; | |
| } | |
| return value; | |
| } | |
| const date = remapPerson("birthdate", new Date()); | |
| const num = remapPerson("age", 42); | |
| const name = remapPerson("name", "John Doe"); | |
| type tests = [ | |
| Expect<Equal<typeof date, Date>>, | |
| Expect<Equal<typeof num, number>>, | |
| Expect<Equal<typeof name, string>>, | |
| ]; | |
| // Infer Types from Type Arguments | |
| export class Component<TProps> { | |
| private props: TProps; | |
| constructor(props: TProps) { | |
| this.props = props; | |
| } | |
| getProps = () => this.props; | |
| } | |
| const cloneComponent = <TProps>(component: Component<TProps>): Component<TProps> => { | |
| return new Component(component.getProps()); | |
| }; | |
| it("Should clone the props from a passed-in Component", () => { | |
| const component = new Component({ a: 1, b: 2, c: 3 }); | |
| const clonedComponent = cloneComponent(component); | |
| const result = clonedComponent.getProps(); | |
| expect(result).toEqual({ a: 1, b: 2, c: 3 }); | |
| type tests = [ | |
| Expect<Equal<typeof result, { a: number; b: number; c: number }>> | |
| ]; | |
| }); | |
| // Strongly Type a Reduce Function | |
| const array = [ | |
| { | |
| name: "John", | |
| }, | |
| { | |
| name: "Steve", | |
| }, | |
| ]; | |
| const obj = array.reduce<Record<string, { name: string }>>((accum, item) => { | |
| accum[item.name] = item; | |
| return accum; | |
| }, {}); | |
| it("Should resolve to an object where name is the key", () => { | |
| expect(obj).toEqual({ | |
| John: { | |
| name: "John", | |
| }, | |
| Steve: { | |
| name: "Steve", | |
| }, | |
| }); | |
| type tests = [Expect<Equal<typeof obj, Record<string, { name: string }>>>]; | |
| }); | |
| // Avoid any Types with Generics | |
| const fetchData = async <TData>(url: string) => { | |
| let data: TData = await fetch(url).then((response) => response.json()); | |
| return data; | |
| }; | |
| it("Should fetch data from an API", async () => { | |
| const data = await fetchData<{ name: string }>( | |
| "https://swapi.py4e.com/api/people/1", | |
| ); | |
| expect(data.name).toEqual("Luke Skywalker"); | |
| type tests = [Expect<Equal<typeof data, { name: string }>>]; | |
| }); | |
| // 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); | |
| }; | |
| describe("getHomePageFeatureFlags", () => { | |
| const EXAMPLE_CONFIG = { | |
| apiEndpoint: "https://api.example.com", | |
| apiVersion: "v1", | |
| apiKey: "1234567890", | |
| rawConfig: { | |
| featureFlags: { | |
| homePage: { | |
| showBanner: true, | |
| showLogOut: false, | |
| }, | |
| loginPage: { | |
| showCaptcha: true, | |
| showConfirmPassword: false, | |
| }, | |
| }, | |
| }, | |
| }; | |
| it("Should return the homePage flag object", () => { | |
| const flags = getHomePageFeatureFlags( | |
| EXAMPLE_CONFIG, | |
| (defaultFlags) => defaultFlags | |
| ); | |
| expect(flags).toEqual({ | |
| showBanner: true, | |
| showLogOut: false, | |
| }); | |
| type tests = [ | |
| Expect<Equal<typeof flags, { showBanner: boolean; showLogOut: boolean }>> | |
| ]; | |
| }); | |
| it("Should allow you to modify the result", () => { | |
| const flags = getHomePageFeatureFlags(EXAMPLE_CONFIG, (defaultFlags) => ({ | |
| ...defaultFlags, | |
| showBanner: false, | |
| })); | |
| expect(flags).toEqual({ | |
| showBanner: false, | |
| showLogOut: false, | |
| }); | |
| type tests = [ | |
| Expect<Equal<typeof flags, { showBanner: boolean; showLogOut: boolean }>> | |
| ]; | |
| }); | |
| }); | |
| // Typed Object Keys | |
| // Two Approaches for Typing Object Keys | |
| const typedObjectKeys = <TKey extends string>(obj: Record<TKey, any>) => { | |
| return Object.keys(obj) as Array<TKey>; | |
| }; | |
| it("Should return the keys of the object", () => { | |
| const result1 = typedObjectKeys({ | |
| a: 1, | |
| b: 2, | |
| }); | |
| expect(result1).toEqual(["a", "b"]); | |
| type test = Expect<Equal<typeof result1, Array<"a" | "b">>>; | |
| }); | |
| // Fixing Errors in Generic Functions | |
| // Fixing the "Not Assignable" Error | |
| type Person = { | |
| name: string; | |
| age: number; | |
| birthdate: Date; | |
| }; | |
| export function remapPerson<Key extends keyof Person>( | |
| key: Key, | |
| value: Person[Key], | |
| ): Person[Key] { | |
| if (key === "birthdate") { | |
| return new Date() as Person[Key]; | |
| } | |
| return value; | |
| } | |
| const date = remapPerson("birthdate", new Date()); | |
| const num = remapPerson("age", 42); | |
| const name = remapPerson("name", "John Doe"); | |
| type tests = [ | |
| Expect<Equal<typeof date, Date>>, | |
| Expect<Equal<typeof num, number>>, | |
| Expect<Equal<typeof name, string>>, | |
| ]; | |
| // Generic Function Currying | |
| export const curryFunction = | |
| <T>(t: T) => | |
| <U>(u: U) => | |
| <V>(v: V) => { | |
| return { | |
| t, | |
| u, | |
| v, | |
| }; | |
| }; | |
| it("Should return an object which matches the types of each input", () => { | |
| const result = curryFunction(1)(2)(3); | |
| expect(result).toEqual({ | |
| t: 1, | |
| u: 2, | |
| v: 3, | |
| }); | |
| type test = [ | |
| Expect<Equal<typeof result, { t: number; u: number; v: number }>>, | |
| ]; | |
| }); | |
| // Generic Interfaces with Functions | |
| export interface Cache<T> { | |
| get: (key: string) => T | undefined; | |
| set: (key: string, value: T) => void; | |
| // You can fix this by only changing the line below! | |
| clone: <U>(transform: (elem: T) => U) => Cache<U>; | |
| } | |
| const createCache = <T>(initialCache?: Record<string, T>): Cache<T> => { | |
| const cache: Record<string, T> = initialCache || {}; | |
| return { | |
| get: (key) => cache[key], | |
| set: (key, value) => { | |
| cache[key] = value; | |
| }, | |
| clone: (transform) => { | |
| const newCache: Record<string, any> = {}; | |
| for (const key in cache) { | |
| newCache[key] = transform(cache[key]); | |
| } | |
| return createCache(newCache); | |
| }, | |
| }; | |
| }; | |
| it("Should let you get and set to/from the cache", () => { | |
| const cache = createCache<number>(); | |
| cache.set("a", 1); | |
| cache.set("b", 2); | |
| expect(cache.get("a")).toEqual(1); | |
| expect(cache.get("b")).toEqual(2); | |
| }); | |
| it("Should let you clone the cache using a transform function", () => { | |
| const numberCache = createCache<number>(); | |
| numberCache.set("a", 1); | |
| numberCache.set("b", 2); | |
| const stringCache = numberCache.clone((elem) => { | |
| return String(elem); | |
| }); | |
| const a = stringCache.get("a"); | |
| expect(a).toEqual("1"); | |
| type tests = [Expect<Equal<typeof a, string | undefined>>]; | |
| }); | |
| // Refactoring Functions with Unnecessary Type Arguments | |
| const returnBothOfWhatIPassIn = < | |
| TParams extends { | |
| a: unknown; | |
| b: unknown; | |
| }, | |
| >( | |
| params: TParams, | |
| ): [TParams["a"], TParams["b"]] => { | |
| return [params.a, params.b]; | |
| }; | |
| it("Should return a tuple of the properties a and b", () => { | |
| const result = returnBothOfWhatIPassIn({ | |
| a: "a", | |
| b: 1, | |
| }); | |
| expect(result).toEqual(["a", 1]); | |
| type test1 = Expect<Equal<typeof result, [string, number]>>; | |
| }); | |
| // Improving Type Inference with Additional Generics | |
| const getValue = <TObj, TKey extends keyof TObj>(obj: TObj, key: TKey) => { | |
| return obj[key]; | |
| }; | |
| const obj = { | |
| a: 1, | |
| b: "some-string", | |
| c: true, | |
| }; | |
| const numberResult = getValue(obj, "a"); | |
| const stringResult = getValue(obj, "b"); | |
| const booleanResult = getValue(obj, "c"); | |
| type tests = [ | |
| Expect<Equal<typeof numberResult, number>>, | |
| Expect<Equal<typeof stringResult, string>>, | |
| Expect<Equal<typeof booleanResult, boolean>>, | |
| ]; | |
| // Create a Factory Function to Apply Type Arguments to All Child Functions | |
| const makeUseStyled = <TTheme = {}>() => { | |
| const useStyled = (func: (theme: TTheme) => CSSProperties) => { | |
| return {} as CSSProperties | |
| } | |
| return useStyled | |
| } | |
| interface MyTheme { | |
| color: { | |
| primary: string; | |
| }; | |
| fontSize: { | |
| small: string; | |
| }; | |
| } | |
| const useStyled = makeUseStyled<MyTheme>() | |
| const buttonStyle = useStyled((theme) => ({ | |
| color: theme.color.primary, | |
| fontSize: theme.fontSize.small, | |
| })) | |
| const divStyle = useStyled((theme) => ({ | |
| backgroundColor: theme.color.primary, | |
| })) | |
| // | |
| // Pass Type Arguments to a Function | |
| export const createSet = <T>() => { | |
| return new Set<T>(); | |
| }; | |
| const stringSet = createSet<string>(); | |
| const numberSet = createSet<number>(); | |
| const unknownSet = createSet(); | |
| type tests = [ | |
| Expect<Equal<typeof stringSet, Set<string>>>, | |
| Expect<Equal<typeof numberSet, Set<number>>>, | |
| Expect<Equal<typeof unknownSet, Set<unknown>>>, | |
| ]; | |
| // | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment