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 optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties readonly
type 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 keys
type 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 type
type 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 names
type 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 types
type Stringify<T> = {
[K in keyof T]: T[K] extends string ? T[K] : string;
};
type Nullify<T> = {
[K in keyof T]: T[K] | null;
};
// Deep transformation
type 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 strings

Property Filtering and Selection

// Pick properties by type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// Omit properties by type
type 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 handling
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? DeepPartial<U>[]
: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
// Deep readonly
type 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 readonly

Template Literal Types Deep Dive

Advanced String Manipulation

// Split string by delimiter
type 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 strings
type 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 substring
type 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 camelCase
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
? `${P1}${Uppercase<P2>}${CamelCase<P3>}`
: S;
// Convert to snake_case
type SnakeCase<S extends string> = S extends `${infer T}${infer U}`
? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
: S;
// Convert to kebab-case
type 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 cases
type 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 methods
type 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 routes
type 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 parameters
type 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 methods
type 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 rules
type 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 schema
type ValidationSchema<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
// Generate error types
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
// Generate validator function type
type 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 schema
interface UserTable {
id: number;
name: string;
email: string;
age: number;
createdAt: Date;
}
// Generate column types
type Columns<T> = keyof T;
type ColumnType<T, K extends keyof T> = T[K];
// Query builder types
type 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>[]>;
};
// Usage
declare 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 depth
type 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 possible
type 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 performance
type OptimizedTransform<T extends string> = T extends any
? T extends `${infer Prefix}_${infer Suffix}`
? `${Prefix}${Capitalize<Suffix>}`
: T
: never;
// Cache commonly used transformations
type 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 utilities
type 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 transformations
type 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 validation
function 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 names
type Transform<T> = { [K in keyof T]: string };
// ✅ Descriptive names
type StringifyProperties<T> = { [K in keyof T]: string };

2. Provide Utility Types for Common Patterns

// Create reusable utilities
type 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 types
type SafeTransform<T> = keyof T extends never
? {}
: { [K in keyof T]: Transform<T[K]> };
// Handle union types properly
type DistributiveTransform<T> = T extends any
? { [K in keyof T]: T[K] }
: never;

Common Pitfalls

1. Forgetting Key Constraints

// ❌ This might not work as expected
type BadTransform<T> = {
[K in keyof T as `get${K}`]: () => T[K];
};
// ✅ Ensure K is a string
type GoodTransform<T> = {
[K in keyof T as `get${string & K}`]: () => T[K];
};

2. Infinite Recursion

// ❌ Can cause infinite recursion
type BadDeepTransform<T> = {
[K in keyof T]: T[K] extends object ? BadDeepTransform<T[K]> : T[K];
};
// ✅ Add depth limit or base case
type 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.

Share Feedback