📚 5 min read
Mapped Types in TypeScript ​
This section explores TypeScript's mapped types and their applications in type transformations.
Overview ​
Mapped types allow you to create new types based on existing ones by transforming their properties in a consistent way.
Basic Mapped Types ​
Property Modifiers ​
typescript
interface User {
id: string;
name: string;
email: string;
}
// Make all properties optional
type PartialUser = {
[P in keyof User]?: User[P];
};
// Make all properties readonly
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
// Make all properties nullable
type NullableUser = {
[P in keyof User]: User[P] | null;
};
Generic Mapped Types ​
typescript
// Generic partial type
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Generic readonly type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Generic nullable type
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
Advanced Patterns ​
Property Remapping ​
typescript
// Add prefix to property names
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
// Example usage
interface Config {
host: string;
port: number;
}
type PrefixedConfig = Prefixed<Config, 'app'>;
// Result:
// {
// appHost: string;
// appPort: number;
// }
// Filter properties by value type
type PickByValueType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface Form {
name: string;
age: number;
isActive: boolean;
tags: string[];
}
type StringProps = PickByValueType<Form, string>;
// Result:
// {
// name: string;
// }
Conditional Type Mapping ​
typescript
// Map types based on conditions
type ConditionalMap<T> = {
[K in keyof T]: T[K] extends Function
? T[K]
: T[K] extends object
? Readonly<T[K]>
: T[K];
};
interface ApiConfig {
endpoint: string;
timeout: number;
retryCount: number;
onSuccess: () => void;
onError: (error: Error) => void;
options: {
headers: Record<string, string>;
params: Record<string, string>;
};
}
type SafeApiConfig = ConditionalMap<ApiConfig>;
// Result: All non-function object properties are readonly
Real-World Example ​
typescript
// Domain types
interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
}
interface Order {
id: string;
userId: string;
items: Array<{
productId: string;
quantity: number;
}>;
status: 'pending' | 'completed' | 'cancelled';
total: number;
}
// Validation rules type
type ValidationRules<T> = {
[K in keyof T]: {
required?: boolean;
min?: number;
max?: number;
pattern?: RegExp;
validate?: (value: T[K]) => boolean;
message?: string;
};
};
// Form field type
type FormField<T> = {
[K in keyof T]: {
value: T[K];
error?: string;
touched: boolean;
dirty: boolean;
};
};
// Implementation
class FormManager<T extends object> {
private fields: FormField<T>;
private rules: ValidationRules<T>;
private initialValues: T;
constructor(initialValues: T, rules: ValidationRules<T>) {
this.initialValues = initialValues;
this.rules = rules;
this.fields = this.initializeFields(initialValues);
}
private initializeFields(values: T): FormField<T> {
const fields = {} as FormField<T>;
for (const key in values) {
fields[key] = {
value: values[key],
touched: false,
dirty: false,
};
}
return fields;
}
setValue<K extends keyof T>(field: K, value: T[K]): void {
this.fields[field] = {
...this.fields[field],
value,
dirty: value !== this.initialValues[field],
};
this.validateField(field);
}
private validateField<K extends keyof T>(field: K): void {
const rules = this.rules[field];
const value = this.fields[field].value;
if (!rules) return;
if (rules.required && !value) {
this.fields[field].error = rules.message || 'Field is required';
return;
}
if (
typeof value === 'number' &&
rules.min !== undefined &&
value < rules.min
) {
this.fields[field].error =
rules.message || `Value must be at least ${rules.min}`;
return;
}
if (
typeof value === 'number' &&
rules.max !== undefined &&
value > rules.max
) {
this.fields[field].error =
rules.message || `Value must be at most ${rules.max}`;
return;
}
if (
typeof value === 'string' &&
rules.pattern &&
!rules.pattern.test(value)
) {
this.fields[field].error =
rules.message || 'Value does not match pattern';
return;
}
if (rules.validate && !rules.validate(value)) {
this.fields[field].error = rules.message || 'Validation failed';
return;
}
delete this.fields[field].error;
}
getFieldState<K extends keyof T>(field: K): FormField<T>[K] {
return this.fields[field];
}
isValid(): boolean {
return Object.values(this.fields).every((field) => !field.error);
}
isDirty(): boolean {
return Object.values(this.fields).some((field) => field.dirty);
}
reset(): void {
this.fields = this.initializeFields(this.initialValues);
}
}
// Usage example
const productRules: ValidationRules<Product> = {
id: {
required: true,
pattern: /^PRD-\d+$/,
message: 'Invalid product ID format',
},
name: {
required: true,
validate: (value) => value.length >= 3,
message: 'Name must be at least 3 characters',
},
price: {
required: true,
min: 0,
message: 'Price must be positive',
},
stock: {
required: true,
min: 0,
message: 'Stock must be positive',
},
category: {
required: true,
},
};
const form = new FormManager<Product>(
{
id: '',
name: '',
price: 0,
stock: 0,
category: '',
},
productRules
);
// Update values
form.setValue('name', 'Product 1');
form.setValue('price', -1); // Will set error
// Check state
console.log(form.getFieldState('price'));
// {
// value: -1,
// error: 'Price must be positive',
// touched: false,
// dirty: true
// }
console.log(form.isValid()); // false
console.log(form.isDirty()); // true
Best Practices ​
Type Mapping:
- Use built-in mapped types when possible
- Create reusable generic mapped types
- Document complex type transformations
Property Remapping:
- Use template literal types for naming
- Consider type inference implications
- Handle edge cases explicitly
Performance:
- Avoid deeply nested mapped types
- Cache complex type computations
- Use type aliases for readability