SOLUTION: Challenge 1 - Build Custom Utilities ================================================ Challenge: Create utility types: GetType (property type), Flatten (array), Nullable (add null to all properties). --- SOLUTION: // 1. GetType — Extract the type of property K from T type GetType = T[K]; interface User { id: number; name: string; email: string; } type UserID = GetType; // number type UserName = GetType; // string --- // 2. Flatten — Unwrap nested arrays type Flatten = T extends (infer U)[] ? Flatten : T; type Test1 = Flatten; // number type Test2 = Flatten; // string type Test3 = Flatten<[1, [2, [3]]]>; // 1 | 2 | 3 --- // 3. Nullable — Make all properties nullable type Nullable = { [K in keyof T]: T[K] | null; }; type NullableUser = Nullable; // { // id: number | null; // name: string | null; // email: string | null; // } const user: NullableUser = { id: null, name: "Alice", email: null }; --- EXPLANATION: GETTYPE: type GetType = T[K]; This is "indexed access" — it accesses the type of property K. K constrained to keyof T ensures K is a valid key. T[K] returns the type of that property. Example: T = User, K = "name" T[K] = User["name"] = string FLATTEN: type Flatten = T extends (infer U)[] ? Flatten : T; Recursive: does T look like an array? - If yes: infer the element type U and recursively flatten - If no: return T (it's a leaf value) Example: Flatten → Is number[][] an array? Yes → infer U = number[] → Flatten → Is number[] an array? Yes → infer U = number → Flatten → Is number an array? No → return number NULLABLE: type Nullable = { [K in keyof T]: T[K] | null; }; Mapped type: iterate over all properties of T. For each property K, the type is T[K] | null. Example: Nullable → { name: string | null, email: string | null, id: number | null } --- COMPOSITION EXAMPLE: You can build more complex types by combining these: // Make array elements nullable type NullableArray = Nullable; // Get array element type type ArrayElement = T extends (infer U)[] ? U : never; // Flatten then make nullable type FlattenedNullable = Nullable>; type Test = FlattenedNullable; // number | null --- ADVANCED: Deep Nullable The basic Nullable only works one level deep. For deeply nested objects: type DeepNullable = { [K in keyof T]: T[K] extends object ? DeepNullable | null : T[K] | null; }; interface Address { street: string; city: string; } interface Person { name: string; address: Address; } type DeepNullablePerson = DeepNullable; // { // name: string | null; // address: DeepNullable
| null // } // // Expands to: // { // name: string | null; // address: { // street: string | null; // city: string | null; // } | null; // } This recursively makes every property (even nested) nullable. --- WHEN TO BUILD CUSTOM UTILITIES: ✅ DO build custom utilities when: - You repeat the same type transformation in multiple places - You want to abstract type logic for reuse - You're building a library or framework ❌ DON'T over-abstract: - Simple one-off transformations don't need utilities - Too many nested generics become unreadable Balance: build utilities for patterns you use 3+ times. --- REAL-WORLD USAGE: Libraries use these patterns everywhere: - ORM libraries (Prisma, TypeORM) build Partial, Select, Include utilities - React Query uses utilities to derive query result types - tRPC builds end-to-end type inference with custom utilities - Express.js middleware chains build request/response type utilities Mastering utilities unlocks the full power of the type system.