Mapped Types & Template Literals
Mapped types are one of TypeScript’s most powerful features for creating new types by transforming existing ones. Combined with template literal types, they enable sophisticated type-level programming that can adapt to your data structures and API designs.
Understanding Mapped Types
Basic Mapped Type Syntax
Mapped types use the syntax { [K in keyof T]: NewType } to transform each property:
// Make all properties optionaltype Partial<T> = { [P in keyof T]?: T[P];};
// Make all properties requiredtype Required<T> = { [P in keyof T]-?: T[P];};
// Make all properties readonlytype Readonly<T> = { readonly [P in keyof T]: T[P];};
interface User { id: number; name: string; email: string;}
type PartialUser = Partial<User>;// { id?: number; name?: string; email?: string; }
type RequiredUser = Required<PartialUser>;// { id: number; name: string; email: string; }Key Remapping with as
TypeScript 4.1 introduced key remapping, allowing you to transform property names:
// Prefix all keystype Prefixed<T, Prefix extends string> = { [K in keyof T as `${Prefix}${string & K}`]: T[K];};
type PrefixedUser = Prefixed<User, "user_">;// { user_id: number; user_name: string; user_email: string; }
// Filter properties by typetype StringProperties<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K];};
type UserStrings = StringProperties<User>;// { name: string; email: string; }
// Transform property namestype Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};
type UserGetters = Getters<User>;// { getId: () => number; getName: () => string; getEmail: () => string; }Advanced Mapped Type Patterns
Conditional Property Transformation
// Transform properties based on their typestype Stringify<T> = { [K in keyof T]: T[K] extends string ? T[K] : string;};
type Nullify<T> = { [K in keyof T]: T[K] | null;};
// Deep transformationtype DeepStringify<T> = { [K in keyof T]: T[K] extends object ? DeepStringify<T[K]> : string;};
interface ComplexUser { id: number; name: string; profile: { age: number; bio: string; settings: { theme: "light" | "dark"; notifications: boolean; }; };}
type StringifiedUser = DeepStringify<ComplexUser>;// All nested properties become stringsProperty Filtering and Selection
// Pick properties by typetype PickByType<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K];};
// Omit properties by typetype OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K];};
interface MixedTypes { id: number; name: string; isActive: boolean; createdAt: Date; tags: string[]; metadata: Record<string, any>;}
type StringProps = PickByType<MixedTypes, string>;// { name: string; }
type NonStringProps = OmitByType<MixedTypes, string>;// { id: number; isActive: boolean; createdAt: Date; tags: string[]; metadata: Record<string, any>; }Recursive Mapped Types
// Deep partial with array handlingtype DeepPartial<T> = { [P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial<U>[] : T[P] extends object ? DeepPartial<T[P]> : T[P];};
// Deep readonlytype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends (infer U)[] ? readonly DeepReadonly<U>[] : T[P] extends object ? DeepReadonly<T[P]> : T[P];};
interface NestedData { users: User[]; settings: { theme: string; features: { darkMode: boolean; notifications: string[]; }; };}
type PartialNestedData = DeepPartial<NestedData>;// All properties and nested properties are optional
type ReadonlyNestedData = DeepReadonly<NestedData>;// All properties and nested properties are readonlyTemplate Literal Types Deep Dive
Advanced String Manipulation
// Split string by delimitertype Split<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
type SplitPath = Split<"user/profile/settings", "/">;// ["user", "profile", "settings"]
// Join array of stringstype Join<T extends string[], D extends string> = T extends readonly [infer F, ...infer R] ? F extends string ? R extends string[] ? R['length'] extends 0 ? F : `${F}${D}${Join<R, D>}` : never : never : '';
type JoinedPath = Join<["api", "v1", "users"], "/">;// "api/v1/users"
// Replace substringtype Replace<S extends string, From extends string, To extends string> = S extends `${infer Prefix}${From}${infer Suffix}` ? `${Prefix}${To}${Replace<Suffix, From, To>}` : S;
type ReplacedString = Replace<"hello-world-example", "-", "_">;// "hello_world_example"Case Transformations
// Convert to camelCasetype CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}` ? `${P1}${Uppercase<P2>}${CamelCase<P3>}` : S;
// Convert to snake_casetype SnakeCase<S extends string> = S extends `${infer T}${infer U}` ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}` : S;
// Convert to kebab-casetype KebabCase<S extends string> = S extends `${infer T}${infer U}` ? `${T extends Capitalize<T> ? "-" : ""}${Lowercase<T>}${KebabCase<U>}` : S;
type CamelCased = CamelCase<"user_profile_settings">; // "userProfileSettings"type SnakeCased = SnakeCase<"UserProfileSettings">; // "_user_profile_settings"type KebabCased = KebabCase<"UserProfileSettings">; // "-user-profile-settings"Combining Mapped Types with Template Literals
// Transform object keys to different casestype CamelCaseKeys<T> = { [K in keyof T as CamelCase<string & K>]: T[K];};
type SnakeCaseKeys<T> = { [K in keyof T as SnakeCase<string & K>]: T[K];};
interface ApiResponse { user_id: number; first_name: string; last_name: string; email_address: string;}
type CamelCasedResponse = CamelCaseKeys<ApiResponse>;// { userId: number; firstName: string; lastName: string; emailAddress: string; }
// Create both getter and setter methodstype AccessorMethods<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];} & { [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;};
type UserAccessors = AccessorMethods<User>;// {// getId: () => number;// getName: () => string;// getEmail: () => string;// setId: (value: number) => void;// setName: (value: string) => void;// setEmail: (value: string) => void;// }Practical Applications
API Route Type Generation
// Define API routestype ApiRoutes = { "GET /users": { response: User[] }; "POST /users": { body: Omit<User, "id">; response: User }; "GET /users/:id": { params: { id: string }; response: User }; "PUT /users/:id": { params: { id: string }; body: Partial<User>; response: User }; "DELETE /users/:id": { params: { id: string }; response: void };};
// Extract route parameterstype ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : {};
// Generate client methodstype ApiClient = { [K in keyof ApiRoutes as K extends `${infer Method} ${infer Path}` ? `${Lowercase<Method>}${CamelCase<Replace<Path, "/", "_">>}` : never ]: K extends `${string} ${infer Path}` ? ( ...args: ApiRoutes[K] extends { params: infer P } ? ApiRoutes[K] extends { body: infer B } ? [params: P, body: B] : [params: P] : ApiRoutes[K] extends { body: infer B } ? [body: B] : [] ) => Promise<ApiRoutes[K] extends { response: infer R } ? R : void> : never;};
// Result:// {// getUsers: () => Promise<User[]>;// postUsers: (body: Omit<User, "id">) => Promise<User>;// getUsersId: (params: { id: string }) => Promise<User>;// putUsersId: (params: { id: string }, body: Partial<User>) => Promise<User>;// deleteUsersId: (params: { id: string }) => Promise<void>;// }Form Validation Schema Generation
// Define validation rulestype ValidationRule<T> = { required?: boolean; min?: T extends string | any[] ? number : T extends number ? T : never; max?: T extends string | any[] ? number : T extends number ? T : never; pattern?: T extends string ? RegExp : never; custom?: (value: T) => boolean | string;};
// Generate validation schematype ValidationSchema<T> = { [K in keyof T]: ValidationRule<T[K]>;};
// Generate error typestype ValidationErrors<T> = { [K in keyof T]?: string[];};
// Generate validator function typetype ValidatorFunction<T> = (data: T) => ValidationErrors<T>;
interface RegistrationForm { username: string; email: string; password: string; age: number; terms: boolean;}
type RegistrationSchema = ValidationSchema<RegistrationForm>;type RegistrationErrors = ValidationErrors<RegistrationForm>;type RegistrationValidator = ValidatorFunction<RegistrationForm>;
const registrationSchema: RegistrationSchema = { username: { required: true, min: 3, max: 20 }, email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }, password: { required: true, min: 8, custom: (pwd) => pwd.length >= 8 }, age: { required: true, min: 18, max: 120 }, terms: { required: true }};Database Query Builder Types
// Define table schemainterface UserTable { id: number; name: string; email: string; age: number; createdAt: Date;}
// Generate column typestype Columns<T> = keyof T;type ColumnType<T, K extends keyof T> = T[K];
// Query builder typestype SelectQuery<T, K extends keyof T = keyof T> = { select<U extends K[]>(...columns: U): SelectQuery<T, U[number]>; where<C extends keyof T>(column: C, operator: "=" | "!=" | ">" | "<", value: T[C]): SelectQuery<T, K>; orderBy<C extends keyof T>(column: C, direction?: "ASC" | "DESC"): SelectQuery<T, K>; limit(count: number): SelectQuery<T, K>; execute(): Promise<Pick<T, K>[]>;};
// Usagedeclare function createQuery<T>(): SelectQuery<T>;
const userQuery = createQuery<UserTable>() .select("name", "email") // Type-safe column selection .where("age", ">", 18) // Type-safe where conditions .orderBy("name", "ASC") // Type-safe ordering .limit(10);
// Result type: Promise<Pick<UserTable, "name" | "email">[]>Performance Considerations
Avoiding Deep Recursion
// Limit recursion depthtype DeepPartial<T, Depth extends number = 5> = Depth extends 0 ? T : { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P], Prev<Depth>> : T[P]; };
type Prev<T extends number> = T extends 5 ? 4 : T extends 4 ? 3 : T extends 3 ? 2 : T extends 2 ? 1 : T extends 1 ? 0 : never;
// Use iterative approach when possibletype FlattenKeys<T, Prefix extends string = ""> = { [K in keyof T]: T[K] extends object ? FlattenKeys<T[K], `${Prefix}${string & K}.`> : `${Prefix}${string & K}`;}[keyof T];
type UserKeys = FlattenKeys<ComplexUser>;// "id" | "name" | "profile.age" | "profile.bio" | "profile.settings.theme" | "profile.settings.notifications"Optimizing Template Literal Types
// Use union distribution for better performancetype OptimizedTransform<T extends string> = T extends any ? T extends `${infer Prefix}_${infer Suffix}` ? `${Prefix}${Capitalize<Suffix>}` : T : never;
// Cache commonly used transformationstype CommonTransforms = { user_id: "userId"; first_name: "firstName"; last_name: "lastName"; created_at: "createdAt"; updated_at: "updatedAt";};
type FastCamelCase<T extends string> = T extends keyof CommonTransforms ? CommonTransforms[T] : CamelCase<T>;Testing Mapped Types
// Type testing utilitiestype Expect<T extends true> = T;type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
// Test mapped type transformationstype TestPartial = Expect<Equal< Partial<{ a: string; b: number }>, { a?: string; b?: number }>>;
type TestCamelCase = Expect<Equal< CamelCase<"hello_world">, "helloWorld">>;
type TestKeyRemapping = Expect<Equal< CamelCaseKeys<{ user_name: string; user_age: number }>, { userName: string; userAge: number }>>;
// Runtime validationfunction validateMappedTypes() { const partial: Partial<User> = { name: "John" }; // id and email are optional const camelCased: CamelCaseKeys<{ user_name: string }> = { userName: "John" };
console.log("Mapped type tests passed!");}Best Practices
1. Use Descriptive Type Names
// ❌ Generic namestype Transform<T> = { [K in keyof T]: string };
// ✅ Descriptive namestype StringifyProperties<T> = { [K in keyof T]: string };2. Provide Utility Types for Common Patterns
// Create reusable utilitiestype Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;type RequiredExcept<T, K extends keyof T> = Required<Omit<T, K>> & Pick<T, K>;
type UserWithOptionalEmail = Optional<User, "email">;type UserRequiredExceptId = RequiredExcept<Partial<User>, "id">;3. Document Complex Transformations
/** * Converts an object type to have all string properties as optional getters * and all other properties as required setters. * * @example * type Result = MixedAccessors<{ name: string; age: number; active: boolean }>; * // { getName?: () => string; setAge: (value: number) => void; setActive: (value: boolean) => void; } */type MixedAccessors<T> = { [K in keyof T as T[K] extends string ? `get${Capitalize<string & K>}` : never ]?: () => T[K];} & { [K in keyof T as T[K] extends string ? never : `set${Capitalize<string & K>}` ]: (value: T[K]) => void;};4. Handle Edge Cases
// Handle empty objects and never typestype SafeTransform<T> = keyof T extends never ? {} : { [K in keyof T]: Transform<T[K]> };
// Handle union types properlytype DistributiveTransform<T> = T extends any ? { [K in keyof T]: T[K] } : never;Common Pitfalls
1. Forgetting Key Constraints
// ❌ This might not work as expectedtype BadTransform<T> = { [K in keyof T as `get${K}`]: () => T[K];};
// ✅ Ensure K is a stringtype GoodTransform<T> = { [K in keyof T as `get${string & K}`]: () => T[K];};2. Infinite Recursion
// ❌ Can cause infinite recursiontype BadDeepTransform<T> = { [K in keyof T]: T[K] extends object ? BadDeepTransform<T[K]> : T[K];};
// ✅ Add depth limit or base casetype GoodDeepTransform<T> = T extends primitive ? T : { [K in keyof T]: GoodDeepTransform<T[K]> };
type primitive = string | number | boolean | null | undefined;Conclusion
Mapped types and template literal types are essential tools for creating flexible, maintainable TypeScript code. They enable:
- Dynamic type transformations based on existing types
- Sophisticated string manipulation at the type level
- Automatic API client generation from route definitions
- Type-safe form validation and schema generation
- Database query builders with full type safety
Mastering these patterns allows you to build incredibly powerful type systems that adapt to your application’s needs while maintaining full type safety.
Next Steps
In the next part, we’ll explore Utility Types & Type Composition, focusing on building reusable type utilities and composing complex type systems from simpler building blocks.