Prerequisites
- Understanding of conditional types ๐ฏ
- Knowledge of keyof operator ๐
- Familiarity with generics โก
What you'll learn
- Master mapped type syntax and patterns ๐๏ธ
- Transform object types systematically ๐ง
- Build powerful utility types ๐จ
- Create type-safe data transformations ๐
๐ฏ Introduction
Welcome to the amazing world of mapped types! ๐ Think of mapped types as TypeScriptโs most powerful transformation tool ๐ง - they can take any existing type and systematically transform every property according to your rules, like having a magical assembly line for types!
Youโre about to discover the secret behind many of TypeScriptโs built-in utilities like Partial
, Required
, and Readonly
. Whether youโre building type-safe APIs ๐, creating data transformation utilities ๐ ๏ธ, or just want to level up your type wizardry ๐งโโ๏ธ, mapped types will revolutionize how you think about type manipulation.
By the end of this tutorial, youโll be transforming types with the precision of a master craftsperson! โจ Letโs begin this transformative journey! ๐
๐ Understanding Mapped Types
๐ค What are Mapped Types?
Mapped types are like automated factories ๐ญ that take an existing type and create a new type by systematically transforming each property. Think of them as โfor loopsโ for types - they iterate over each property key and apply transformation rules.
The basic syntax looks like this:
type MappedType<T> = {
[K in keyof T]: TransformationRule
}
This reads as: โFor each key K in type T, create a new property with some transformation ruleโ
๐จ Your First Mapped Type
Letโs start with a simple example:
// ๐ฏ Original interface
interface User {
id: number;
name: string;
email: string;
age: number;
}
// ๐ช Make all properties optional
type PartialUser<T> = {
[K in keyof T]?: T[K]; // The ? makes each property optional
};
// ๐งช Testing our mapped type
type OptionalUser = PartialUser<User>;
// Result: {
// id?: number;
// name?: string;
// email?: string;
// age?: number;
// }
// ๐ Make all properties readonly
type ReadonlyUser<T> = {
readonly [K in keyof T]: T[K]; // The readonly makes each property immutable
};
// โจ Testing readonly version
type ImmutableUser = ReadonlyUser<User>;
// Result: {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// readonly age: number;
// }
๐ก The Magic: Mapped types automatically iterate over every property and apply your transformation!
๐ Breaking Down the Syntax
type MyMappedType<T> = {
[K in keyof T]: SomeType
// ^ ^ ^ ^
// | | | โโโ The type for each property
// | | โโโโโโโโโ All keys of type T
// | โโโโโโโโโโโโโโโโ "in" operator for iteration
// โโโโโโโโโโโโโโโโโโโโ The property key variable
}
๐ง Basic Mapped Type Patterns
๐ Essential Transformation Recipes
Letโs explore the most useful mapped type patterns:
// ๐จ Make all properties strings (serialization)
type Stringify<T> = {
[K in keyof T]: string;
};
// ๐งช Testing stringification
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
type StringifiedProduct = Stringify<Product>;
// Result: {
// id: string;
// name: string;
// price: string;
// inStock: string;
// }
// ๐ Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// ๐ฎ Testing nullable transformation
type NullableProduct = Nullable<Product>;
// Result: {
// id: number | null;
// name: string | null;
// price: number | null;
// inStock: boolean | null;
// }
// โจ Wrap all properties in arrays
type Arrayify<T> = {
[K in keyof T]: T[K][];
};
// ๐งช Testing arrayification
type ArrayProduct = Arrayify<Product>;
// Result: {
// id: number[];
// name: string[];
// price: number[];
// inStock: boolean[];
// }
๐ฏ Advanced Transformations
// ๐จ Create getter functions for each property
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// ๐งช Testing getter creation
type UserGetters = Getters<User>;
// Result: {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// getAge: () => number;
// }
// ๐ Create setter functions for each property
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
// ๐ฎ Testing setter creation
type UserSetters = Setters<User>;
// Result: {
// setId: (value: number) => void;
// setName: (value: string) => void;
// setEmail: (value: string) => void;
// setAge: (value: number) => void;
// }
// โจ Combine getters and setters
type GettersAndSetters<T> = Getters<T> & Setters<T>;
type UserAccessors = GettersAndSetters<User>;
// Result: Complete getter and setter interface!
๐ก Practical Examples
๐ Example 1: API Response Transformer
Letโs build a system to transform API responses safely:
// ๐ฏ Define different API response formats
interface RawApiUser {
user_id: number;
full_name: string;
email_address: string;
date_of_birth: string; // ISO string from API
is_active: boolean;
created_at: string; // ISO string
updated_at: string; // ISO string
}
interface RawApiProduct {
product_id: number;
product_name: string;
price_cents: number; // Price in cents
category_id: number;
is_available: boolean;
created_date: string; // Different date format
}
// ๐ Transform snake_case API keys to camelCase
type CamelCaseKeys<T> = {
[K in keyof T as CamelCase<K & string>]: T[K];
};
// Helper type for camelCase conversion
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
? `${P1}${Uppercase<P2>}${CamelCase<P3>}`
: S;
// ๐งช Testing camelCase transformation
type CamelUser = CamelCaseKeys<RawApiUser>;
// Result: {
// userId: number;
// fullName: string;
// emailAddress: string;
// dateOfBirth: string;
// isActive: boolean;
// createdAt: string;
// updatedAt: string;
// }
// ๐จ Smart type transformation for frontend
type TransformApiResponse<T> = {
[K in keyof T]:
// Transform date strings to Date objects
T[K] extends string
? K extends `${string}date${string}` | `${string}Date${string}` | `${string}_at`
? Date
: T[K]
// Transform price_cents to price (number to number, but semantically different)
: K extends 'price_cents' | 'priceCents'
? number // Will be divided by 100 in runtime
: T[K];
};
// โจ Complete transformation pipeline
type FrontendUser = TransformApiResponse<CamelCaseKeys<RawApiUser>>;
// Result: {
// userId: number;
// fullName: string;
// emailAddress: string;
// dateOfBirth: Date; // Transformed!
// isActive: boolean;
// createdAt: Date; // Transformed!
// updatedAt: Date; // Transformed!
// }
// ๐ฎ Type-safe transformer class
class ApiTransformer {
// ๐ Transform user data with full type safety
transformUser(rawUser: RawApiUser): FrontendUser {
return {
userId: rawUser.user_id,
fullName: rawUser.full_name,
emailAddress: rawUser.email_address,
dateOfBirth: new Date(rawUser.date_of_birth),
isActive: rawUser.is_active,
createdAt: new Date(rawUser.created_at),
updatedAt: new Date(rawUser.updated_at)
} as FrontendUser;
}
// ๐ฏ Generic transformer for any API response
transformResponse<T extends Record<string, any>>(
rawData: T,
transformer: (raw: T) => TransformApiResponse<CamelCaseKeys<T>>
): TransformApiResponse<CamelCaseKeys<T>> {
return transformer(rawData);
}
}
// ๐งช Usage example
const apiTransformer = new ApiTransformer();
const rawUserData: RawApiUser = {
user_id: 123,
full_name: "Alice Wonder",
email_address: "[email protected]",
date_of_birth: "1990-05-15T00:00:00Z",
is_active: true,
created_at: "2023-01-15T10:30:00Z",
updated_at: "2023-12-01T14:45:00Z"
};
const frontendUser = apiTransformer.transformUser(rawUserData);
console.log("๐ Transformed user:", frontendUser);
// TypeScript knows frontendUser.dateOfBirth is a Date object!
๐ฎ Example 2: Form Validation Schema Generator
Letโs create a type-safe form validation system:
// ๐ Define form interfaces
interface UserRegistrationForm {
username: string;
email: string;
password: string;
confirmPassword: string;
age: number;
termsAccepted: boolean;
newsletter: boolean;
}
interface ProductForm {
name: string;
description: string;
price: number;
category: string;
tags: string[];
featured: boolean;
}
// ๐ฏ Create validation rules for each field type
type ValidationRule<T> =
T extends string
? {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: T) => string | null;
}
: T extends number
? {
required?: boolean;
min?: number;
max?: number;
integer?: boolean;
custom?: (value: T) => string | null;
}
: T extends boolean
? {
required?: boolean;
mustBeTrue?: boolean;
custom?: (value: T) => string | null;
}
: T extends any[]
? {
required?: boolean;
minItems?: number;
maxItems?: number;
custom?: (value: T) => string | null;
}
: {
required?: boolean;
custom?: (value: T) => string | null;
};
// ๐ Generate validation schema for any form
type ValidationSchema<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
// ๐จ Generate error types for each field
type FormErrors<T> = {
[K in keyof T]?: string;
};
// โจ Create form state type
type FormState<T> = {
values: T;
errors: FormErrors<T>;
touched: { [K in keyof T]?: boolean };
isValid: boolean;
isSubmitting: boolean;
};
// ๐งช Type-safe form validator
class FormValidator<T extends Record<string, any>> {
constructor(private schema: ValidationSchema<T>) {}
// ๐ฏ Validate individual field
validateField<K extends keyof T>(
fieldName: K,
value: T[K]
): string | null {
const rule = this.schema[fieldName];
if (!rule) return null;
// Required validation
if (rule.required && (value === null || value === undefined || value === '')) {
return `${String(fieldName)} is required`;
}
// Type-specific validations
if (typeof value === 'string' && 'minLength' in rule) {
if (rule.minLength && value.length < rule.minLength) {
return `${String(fieldName)} must be at least ${rule.minLength} characters`;
}
}
if (typeof value === 'number' && 'min' in rule) {
if (rule.min !== undefined && value < rule.min) {
return `${String(fieldName)} must be at least ${rule.min}`;
}
}
if (typeof value === 'boolean' && 'mustBeTrue' in rule) {
if (rule.mustBeTrue && !value) {
return `${String(fieldName)} must be accepted`;
}
}
// Custom validation
if (rule.custom) {
return rule.custom(value);
}
return null;
}
// ๐ Validate entire form
validateForm(values: T): FormErrors<T> {
const errors: FormErrors<T> = {};
for (const fieldName in values) {
const error = this.validateField(fieldName, values[fieldName]);
if (error) {
errors[fieldName] = error;
}
}
return errors;
}
// โจ Check if form is valid
isFormValid(values: T): boolean {
const errors = this.validateForm(values);
return Object.keys(errors).length === 0;
}
}
// ๐ฎ Usage example
const userRegistrationSchema: ValidationSchema<UserRegistrationForm> = {
username: {
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
required: true,
minLength: 8,
custom: (value) => {
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return "Password must contain uppercase, lowercase, and number";
}
return null;
}
},
confirmPassword: {
required: true,
custom: (value) => {
// Note: In real implementation, you'd have access to other form values
return null; // Would compare with password field
}
},
age: {
required: true,
min: 13,
max: 120,
integer: true
},
termsAccepted: {
required: true,
mustBeTrue: true
},
newsletter: {
required: false
}
};
// ๐งช Create validator and test
const userValidator = new FormValidator(userRegistrationSchema);
const testFormData: UserRegistrationForm = {
username: "alice123",
email: "[email protected]",
password: "SecurePass123",
confirmPassword: "SecurePass123",
age: 25,
termsAccepted: true,
newsletter: false
};
const validationErrors = userValidator.validateForm(testFormData);
const isValid = userValidator.isFormValid(testFormData);
console.log("๐ Validation errors:", validationErrors);
console.log("โ
Form is valid:", isValid);
๐ Advanced Mapped Type Techniques
๐งโโ๏ธ Conditional Property Transformation
// ๐จ Transform properties based on their types
type SmartTransform<T> = {
[K in keyof T]:
T[K] extends string
? `validated_${T[K]}` // Prefix strings
: T[K] extends number
? T[K] | null // Make numbers nullable
: T[K] extends boolean
? T[K] // Keep booleans as-is
: never; // Exclude other types
};
// ๐งช Testing conditional transformation
interface MixedData {
name: string;
count: number;
active: boolean;
metadata: object; // This will become never
}
type TransformedData = SmartTransform<MixedData>;
// Result: {
// name: `validated_${string}`;
// count: number | null;
// active: boolean;
// metadata: never; // Effectively removed
// }
๐๏ธ Recursive Mapped Types
// ๐ฏ Deep readonly transformation
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]> // Recursively apply to nested objects
: T[K];
};
// ๐งช Testing deep readonly
interface NestedData {
user: {
profile: {
name: string;
settings: {
theme: string;
notifications: boolean;
};
};
posts: Array<{
title: string;
content: string;
}>;
};
}
type DeepReadonlyData = DeepReadonly<NestedData>;
// Result: All nested properties become readonly!
// ๐ Deep partial transformation
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
type PartialNestedData = DeepPartial<NestedData>;
// Result: All nested properties become optional!
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Index Signature Issues
// โ Problem with index signatures
interface FlexibleObject {
name: string;
[key: string]: any; // Index signature
}
type StrictTransform<T> = {
[K in keyof T]: string;
};
type TransformedFlexible = StrictTransform<FlexibleObject>;
// Result: { [x: string]: string; name: string } - not what we wanted!
// โ
Solution: Handle index signatures explicitly
type SafeTransform<T> = {
[K in keyof T as K extends string ? K : never]: T[K] extends any ? string : never;
};
type SafeTransformedFlexible = SafeTransform<FlexibleObject>;
// Result: { name: string } - much cleaner!
๐คฏ Pitfall 2: Preserving Modifiers
// โ Losing optional modifiers
interface PartialUser {
id: number;
name?: string;
email?: string;
}
type BadTransform<T> = {
[K in keyof T]: T[K] | null; // Lost the optional modifiers!
};
type LostModifiers = BadTransform<PartialUser>;
// Result: { id: number | null; name: string | null; email: string | null }
// The ? modifiers are gone!
// โ
Preserve modifiers explicitly
type GoodTransform<T> = {
[K in keyof T]?: T[K] | null; // Explicitly preserve optionality
};
// โ
Or use conditional preservation
type PreserveModifiers<T> = {
[K in keyof T as T[K] extends undefined ? never : K]: T[K];
} & {
[K in keyof T as T[K] extends undefined ? K : never]?: T[K];
};
๐ Pitfall 3: Type Distribution Issues
// โ Unexpected distribution behavior
type DistributeProblem<T> = T extends any
? { [K in keyof T]: T[K] }
: never;
type UnionTest = DistributeProblem<{ a: string } | { b: number }>;
// Result distributes: { a: string } | { b: number } (might not be desired)
// โ
Prevent distribution when needed
type NoDistribute<T> = [T] extends [any]
? { [K in keyof T]: T[K] }
: never;
type NoDistributionTest = NoDistribute<{ a: string } | { b: number }>;
// Result: { a: string; b: number } (intersection, not union)
๐ ๏ธ Best Practices
- ๐ฏ Keep Transformations Simple: Start with basic patterns before attempting complex logic
- ๐ Use Descriptive Names:
UserFormFields<T>
is better thanTransform<T>
- ๐ก๏ธ Handle Edge Cases: Consider
never
,unknown
, and union types - ๐จ Preserve Type Information: Donโt lose important modifiers like
readonly
or?
- โจ Test Your Patterns: Always verify your mapped types work as expected
- ๐ Use Conditional Logic Sparingly: Too many conditions make types hard to understand
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Database ORM
Create a mapped type system for a type-safe database ORM:
๐ Requirements:
- โ Transform database column types to TypeScript types
- ๐ง Generate query builder methods for each column
- ๐จ Create type-safe WHERE clause builders
- ๐ Handle relationships between tables
- โจ Type-safe result mapping!
๐ Bonus Points:
- Add transaction support with type safety
- Create migration type checking
- Build query optimization hints
๐ก Solution
๐ Click to see solution
// ๐ฏ Database schema definitions
interface DatabaseColumns {
id: number;
created_at: Date;
updated_at: Date;
}
interface UserTable extends DatabaseColumns {
username: string;
email: string;
age: number;
is_active: boolean;
profile_json: object;
}
interface PostTable extends DatabaseColumns {
title: string;
content: string;
author_id: number; // Foreign key to UserTable
published: boolean;
view_count: number;
}
// ๐ Transform database types to TypeScript types
type DatabaseToTypescript<T> = {
[K in keyof T]:
T[K] extends number ? number :
T[K] extends string ? string :
T[K] extends boolean ? boolean :
T[K] extends Date ? Date :
T[K] extends object ? any :
T[K];
};
// ๐จ Generate query builder methods
type QueryMethods<T> = {
[K in keyof T as `where${Capitalize<string & K>}`]: (
operator: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'LIKE',
value: T[K]
) => QueryBuilder<T>;
} & {
[K in keyof T as `orderBy${Capitalize<string & K>}`]: (
direction?: 'ASC' | 'DESC'
) => QueryBuilder<T>;
};
// โจ Create select field options
type SelectFields<T> = {
[K in keyof T]?: boolean;
} | '*';
// ๐งช Type-safe query builder
class QueryBuilder<T extends Record<string, any>> {
private tableName: string;
private whereConditions: string[] = [];
private orderConditions: string[] = [];
private selectFields: string[] = [];
constructor(tableName: string) {
this.tableName = tableName;
}
// ๐ฏ Type-safe WHERE clause building
where<K extends keyof T>(
field: K,
operator: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'LIKE',
value: T[K]
): this {
this.whereConditions.push(`${String(field)} ${operator} ${JSON.stringify(value)}`);
return this;
}
// ๐ Type-safe ORDER BY
orderBy<K extends keyof T>(
field: K,
direction: 'ASC' | 'DESC' = 'ASC'
): this {
this.orderConditions.push(`${String(field)} ${direction}`);
return this;
}
// ๐จ Type-safe SELECT fields
select<K extends keyof T>(
fields: K[] | '*'
): Pick<T, K extends keyof T ? K : never>[] {
if (fields === '*') {
this.selectFields = ['*'];
} else {
this.selectFields = fields.map(String);
}
// Mock implementation - would execute actual SQL
console.log(`๐ SELECT ${this.selectFields.join(', ')} FROM ${this.tableName}`);
if (this.whereConditions.length > 0) {
console.log(`๐ WHERE ${this.whereConditions.join(' AND ')}`);
}
if (this.orderConditions.length > 0) {
console.log(`๐ ORDER BY ${this.orderConditions.join(', ')}`);
}
return [] as any; // Mock return
}
// โจ Get first result
first(): T | null {
console.log(`๐ฏ ${this.buildQuery()} LIMIT 1`);
return null; // Mock return
}
// ๐ Count results
count(): number {
console.log(`๐ข SELECT COUNT(*) FROM ${this.tableName}`);
return 0; // Mock return
}
private buildQuery(): string {
let query = `SELECT * FROM ${this.tableName}`;
if (this.whereConditions.length > 0) {
query += ` WHERE ${this.whereConditions.join(' AND ')}`;
}
if (this.orderConditions.length > 0) {
query += ` ORDER BY ${this.orderConditions.join(', ')}`;
}
return query;
}
}
// ๐ฎ Database ORM class
class TypeSafeORM {
// ๐ Create table query builder
table<T extends Record<string, any>>(tableName: string): QueryBuilder<T> {
return new QueryBuilder<T>(tableName);
}
// ๐ฏ Type-safe table methods
users(): QueryBuilder<UserTable> {
return new QueryBuilder<UserTable>('users');
}
posts(): QueryBuilder<PostTable> {
return new QueryBuilder<PostTable>('posts');
}
// โจ Join operations with type safety
join<T1 extends Record<string, any>, T2 extends Record<string, any>>(
table1: QueryBuilder<T1>,
table2: QueryBuilder<T2>,
condition: string
): QueryBuilder<T1 & T2> {
// Mock implementation for joins
console.log(`๐ JOIN operation: ${condition}`);
return new QueryBuilder<T1 & T2>('joined_tables');
}
}
// ๐งช Usage examples
const orm = new TypeSafeORM();
// โ
Type-safe queries
const activeUsers = orm.users()
.where('is_active', '=', true) // TypeScript knows is_active is boolean
.where('age', '>=', 18) // TypeScript knows age is number
.orderBy('created_at', 'DESC') // TypeScript knows created_at exists
.select(['username', 'email']); // TypeScript enforces valid field names
const recentPosts = orm.posts()
.where('published', '=', true)
.where('view_count', '>', 100)
.orderBy('created_at', 'DESC')
.select('*');
// โ These would cause TypeScript errors:
// orm.users().where('invalid_field', '=', 'value'); // invalid_field doesn't exist
// orm.users().where('age', '=', 'string'); // age should be number
// orm.posts().orderBy('nonexistent_field'); // field doesn't exist
console.log("๐ Type-safe ORM queries executed!");
// ๐ Advanced: Generate model classes from table types
type ModelMethods<T> = {
save(): Promise<T>;
delete(): Promise<boolean>;
update(data: Partial<T>): Promise<T>;
reload(): Promise<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;
};
class BaseModel<T extends Record<string, any>> implements ModelMethods<T> {
constructor(private data: T) {}
save(): Promise<T> {
console.log("๐พ Saving model...");
return Promise.resolve(this.data);
}
delete(): Promise<boolean> {
console.log("๐๏ธ Deleting model...");
return Promise.resolve(true);
}
update(data: Partial<T>): Promise<T> {
console.log("โ๏ธ Updating model...", data);
Object.assign(this.data, data);
return Promise.resolve(this.data);
}
reload(): Promise<T> {
console.log("๐ Reloading model...");
return Promise.resolve(this.data);
}
// Dynamic getter/setter generation would happen here
[key: string]: any;
}
// ๐ฏ Specific model classes
class User extends BaseModel<UserTable> {
getId() { return this.data.id; }
getUsername() { return this.data.username; }
getEmail() { return this.data.email; }
setUsername(username: string) { this.data.username = username; }
setEmail(email: string) { this.data.email = email; }
private data!: UserTable; // Override for proper typing
}
// Usage
const user = new User({
id: 1,
username: "alice",
email: "[email protected]",
age: 30,
is_active: true,
profile_json: {},
created_at: new Date(),
updated_at: new Date()
});
console.log("๐ค User model created:", user.getUsername());
๐ Key Takeaways
Youโve mastered mapped types! Hereโs what you can now do:
- โ Transform any object type systematically with surgical precision ๐ช
- โ Build powerful utility types like the TypeScript built-ins ๐ก๏ธ
- โ Create type-safe data transformations for APIs and forms ๐ฏ
- โ Handle complex type manipulations with confidence ๐
- โ Design elegant type systems that scale beautifully ๐
Remember: Mapped types are like having a magic wand โจ that can transform any type according to your exact specifications!
๐ค Next Steps
Congratulations! ๐ Youโve conquered mapped types!
Hereโs what to explore next:
- ๐ป Practice with the ORM exercise above - try different table schemas
- ๐๏ธ Build your own mapped type utilities for real projects
- ๐ Move on to our next tutorial: Key Remapping in Mapped Types: Advanced Transformations
- ๐ Share your mapped type creations with the TypeScript community!
You now possess one of TypeScriptโs most powerful transformation tools. Use it to create type systems that are both elegant and practical. Remember - every type transformation expert started with curiosity. Keep experimenting, keep learning, and most importantly, have fun transforming types! ๐โจ
Happy type transforming! ๐๐จโจ