Mapped Types trong TypeScript
Bài 20 – Mapped Types và cách tạo kiểu động dựa trên kiểu khác theo cách đơn giản, trực quan và áp dụng ngay.
Mapped Types
Mapped Types cho phép bạn tạo kiểu mới dựa trên kiểu hiện có, bằng cách lặp qua key và áp dụng phép biến đổi. Đây là nền tảng của nhiều utility types (Partial, Required, Readonly, Pick, Record…) và là kỹ thuật mạnh khi thiết kế data models, form, DTO, hoặc schema tự động.
2. Nội dung chính
- Mapped Types là gì và tại sao quan trọng
- Cú pháp cơ bản [K in keyof T]
- Thay đổi modifier: readonly, optional
- Remap key (as) trong TS 4.1+
- Ứng dụng thực tế: DTO, form schema, API model transformer
3. Ví dụ chi tiết
3.1. Mapped type cơ bản
// Base type
type User = {
id: number;
name: string;
active: boolean;
};
// Tạo mapped type - optional tất cả properties
type OptionalUser = {
[K in keyof User]?: User[K];
};
// Tương đương với Partial<User>
type PartialUser = Partial<User>;
// Kiểm tra kết quả
type TestOptional = OptionalUser;
// { id?: number; name?: string; active?: boolean }
type TestPartial = PartialUser;
// { id?: number; name?: string; active?: boolean }
3.2. Tạo kiểu Readonly từ T
// Generic readonly type
type ReadonlyType<T> = {
readonly [K in keyof T]: T[K];
};
// Sử dụng
type ReadonlyUser = ReadonlyType<User>;
// { readonly id: number; readonly name: string; readonly active: boolean }
// Tương đương built-in Readonly
type BuiltinReadonlyUser = Readonly<User>;
3.3. Xóa modifier (readonly → mutable)
// Remove readonly modifier
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Ví dụ với readonly type
type ReadonlyUser = Readonly<User>;
type MutableUser = Mutable<ReadonlyUser>;
// { id: number; name: string; active: boolean } - không còn readonly
// Remove optional modifier
type RequiredType<T> = {
[K in keyof T]-?: T[K];
};
type OptionalUser = Partial<User>;
type RequiredUser = RequiredType<OptionalUser>;
// { id: number; name: string; active: boolean } - không còn optional
3.4. Remap key (TS 4.1+)
// Rename keys với prefix
type RenameKeys<T> = {
[K in keyof T as `new_${string & K}`]: T[K];
};
type RenamedUser = RenameKeys<User>;
/*
{
new_id: number;
new_name: string;
new_active: boolean;
}
*/
// Remap với điều kiện phức tạp
type CapitalizeKeys<T> = {
[K in keyof T as Capitalize<string & K>]: T[K];
};
type CapitalizedUser = CapitalizeKeys<User>;
/*
{
Id: number;
Name: string;
Active: boolean;
}
*/
3.5. Mapped types kết hợp điều kiện
// Chỉ lấy boolean fields
type OnlyBoolean<T> = {
[K in keyof T as T[K] extends boolean ? K : never]: T[K];
};
type BoolFields = OnlyBoolean<User>;
// { active: boolean }
// Chỉ lấy string fields
type OnlyString<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringFields = OnlyString<User>;
// { name: string }
// Exclude specific types
type ExcludeString<T> = {
[K in keyof T as T[K] extends string ? never : K]: T[K];
};
type NonStringFields = ExcludeString<User>;
// { id: number; active: boolean }
3.6. Tạo DTO từ type domain
// Convert Date to string cho API
type ApiDTO<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K];
};
type Product = {
id: number;
title: string;
createdAt: Date;
updatedAt: Date;
price: number;
};
type ProductDTO = ApiDTO<Product>;
/*
{
id: number;
title: string;
createdAt: string;
updatedAt: string;
price: number;
}
*/
// Advanced DTO với nested types
type DeepApiDTO<T> = T extends Date
? string
: T extends object
? { [K in keyof T]: DeepApiDTO<T[K]> }
: T;
type NestedProduct = {
id: number;
details: {
title: string;
createdAt: Date;
metadata: {
lastViewed: Date;
tags: string[];
};
};
};
type NestedProductDTO = DeepApiDTO<NestedProduct>;
3.7. Form validation schema
// Tạo validation rules từ type
type ValidationRules<T> = {
[K in keyof T]: {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
message?: string;
};
};
type LoginForm = {
username: string;
password: string;
rememberMe: boolean;
};
type LoginValidationRules = ValidationRules<LoginForm>;
/*
{
username: { required?: boolean; minLength?: number; ... };
password: { required?: boolean; minLength?: number; ... };
rememberMe: { required?: boolean; ... };
}
*/
// Default validation rules
const loginValidation: LoginValidationRules = {
username: {
required: true,
minLength: 3,
maxLength: 20,
message: "Username must be 3-20 characters"
},
password: {
required: true,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
message: "Password must contain uppercase, lowercase and number"
},
rememberMe: {
required: false
}
};
3.8. Database entity transformations
// Entity to database schema
type EntityToDb<T> = {
[K in keyof T as `${string & K}_column`]: T[K];
};
type UserEntity = {
id: number;
email: string;
createdAt: Date;
};
type UserDbSchema = EntityToDb<UserEntity>;
/*
{
id_column: number;
email_column: string;
createdAt_column: Date;
}
*/
// Database result to entity
type DbToEntity<T> = {
[K in keyof T as K extends `${infer P}_column` ? P : K]: T[K];
};
type UserDbResult = {
id_column: number;
email_column: string;
createdAt_column: Date;
};
type UserFromDb = DbToEntity<UserDbResult>;
// { id: number; email: string; createdAt: Date }
3. Kiến thức trọng tâm
3.1. Mapped Types là nền tảng để “biến đổi” kiểu dựa trên kiểu khác
- Hầu hết các utility types của TypeScript đều xây dựng từ đây
- Cho phép tạo type transformations phức tạp
- Foundation cho generic programming trong TypeScript
3.2. Modifier (+/- readonly, +/– optional) cho phép điều khiển cấu trúc cực linh hoạt
- Hữu ích khi xử lý form, cập nhật dữ liệu, hoặc tạo bản sao domain model
- Giúp maintain type safety trong transformations
- Cho phép reverse transformations (readonly ↔ mutable)
3.3. Remap key (as) mở ra khả năng tạo schema động
- Dùng cho DTO, API transformer, validation schema…
- Cho phép rename keys với patterns phức tạp
- Enable conditional key mapping
💡 GHI NHỚ: Mapped types là công cụ mạnh nhất trong TypeScript cho type transformations
4. Tài liệu tham khảo
5. Bài tập thực hành
Bài 1: Tạo type Nullable
// Biến tất cả field của T thành T[K] | null
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Test với User type
type nullableUser = nullable<User>;
/*
{
id: number | null;
name: string | null;
active: boolean | null;
}
*/
// Sử dụng
const nullableUser: nullableUser = {
id: null,
name: null,
active: true
};
Bài 2: Tạo type ApiStringify
// Biến tất cả field kiểu number thành string
type ApiStringify<T> = {
[K in keyof T]: T[K] extends number ? string : T[K];
};
// Test với Product type
type StringifiedProduct = ApiStringify<Product>;
/*
{
id: string; // number → string
title: string; // string → string
createdAt: Date; // Date → Date (giữ nguyên)
updatedAt: Date; // Date → Date (giữ nguyên)
price: string; // number → string
}
*/
// Advanced: Handle nested objects
type DeepApiStringify<T> = T extends number
? string
: T extends object
? { [K in keyof T]: DeepApiStringify<T[K]> }
: T;
type DeepStringifiedProduct = DeepApiStringify<Product>;
Bài 3: Tạo type PrefixKeys<T, P>
// Thêm prefix P vào toàn bộ key
type PrefixKeys<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
// Test
type PrefixedUser = PrefixKeys<User, "api_">;
/*
{
api_id: number;
api_name: string;
api_active: boolean;
}
*/
// Với prefix khác nhau
type DbPrefixedUser = PrefixKeys<User, "db_">;
/*
{
db_id: number;
db_name: string;
db_active: boolean;
}
*/
// Generic prefix function
type WithPrefix<T, P extends string> = PrefixKeys<T, P>;
const createApiType = <T>() => <P extends string>(prefix: P): WithPrefix<T, P> => {
// Implementation would go here
return {} as WithPrefix<T, P>;
};
Bài 4: Tạo Event Types từ State
// Tạo event types từ state type
type State = {
loading: boolean;
data: string[];
error: string | null;
};
// Tạo event types với "SET_" prefix
type StateEvents = {
[K in keyof State as `SET_${Uppercase<string & K>}`]: State[K];
};
/*
{
SET_LOADING: boolean;
SET_DATA: string[];
SET_ERROR: string | null;
}
*/
// Tạo action creators
type ActionCreators = {
[K in keyof StateEvents]: (payload: StateEvents[K]) => {
type: K;
payload: StateEvents[K];
};
};
// Sử dụng
declare const actions: ActionCreators;
const setLoading = actions.SET_LOADING(true);
// { type: "SET_LOADING", payload: true }
const setData = actions.SET_DATA(["item", "item"]);
// { type: "SET_DATA", payload: ["item", "item"] }
5. Sai lầm thường gặp
- Quá phức tạp với mapped types - đôi khi simple types đủ tốt
- **Không handle edge cases - recursive types, circular references
- **Quên test kết quả - mapped types có thể unexpected behavior
- **Không document transformations - team khác khó hiểu
- **Over-engineering - không cần mapped types cho mọi use case
⚠️ GHI NHỚ: Mapped types powerful nhưng cần cân nhắc khi sử dụng
6. Kết luận
Mapped Types giúp bạn xây dựng hệ thống kiểu động mạnh mẽ, tái sử dụng tốt và giảm lặp lại khi thiết kế cấu trúc dữ liệu. Đây là bước quan trọng để hiểu các thư viện framework phức tạp và tự xây dựng model thông minh.
💡 GHI NHỚ QUAN TRỌNG: Mapped types là một foundation utility type. Hiểu và dùng tốt nó giúp bạn:
- Practice với real-world examples: Validation, API transformation, v.v.
- Combine với conditional types: Tạo ra các powerful utilities.
- Document your transformations: Giúp team hiểu và maintain code.
- Test và validate kết quả: Ensure types hoạt động như expected.