Deep Dive: Kiểu Nâng Cao trong TypeScript

Chào mừng đến Deep Dive
Bạn đã học nền tảng TypeScript — kiểu cơ bản, interface, generic và utility type. Bây giờ là lúc đi sâu hơn. Bài viết này bao gồm lập trình ở cấp độ kiểu nâng cao giúp phân biệt TypeScript thông thường với kiến trúc thực sự type-safe.
Đây không phải bài tập lý thuyết. Mỗi pattern ở đây giải quyết một vấn đề thực tế: trích xuất kiểu từ hàm, xây dựng event emitter type-safe, tạo branded ID không thể nhầm lẫn, và sinh kiểu từ template chuỗi.
Điều kiện tiên quyết: Giai đoạn 1: Nền tảng TypeScript Thời gian cam kết: 2-3 giờ (thực hành các ví dụ)
Bạn sẽ học được gì
✅ Conditional type với extends và infer
✅ Mapped type và key remapping
✅ Template literal type cho thao tác chuỗi
✅ Recursive type cho cấu trúc lồng nhau
✅ Discriminated union và exhaustive checking
✅ Branded type cho nominal typing
✅ Variadic tuple type
✅ Const assertion và toán tử satisfies
✅ Pattern thực tế: API client type-safe
1. Conditional Type
Conditional type cho phép bạn diễn đạt logic if/else ở cấp kiểu. Cú pháp giống toán tử ba ngôi:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true (string literal extends string)Distributive Conditional Type
Khi conditional type hoạt động trên union, nó phân phối qua từng thành viên:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (KHÔNG PHẢI (string | number)[])Điều này rất mạnh mẽ để lọc union:
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = "hello" | 42 | "world" | true;
type OnlyStrings = ExtractStrings<Mixed>;
// "hello" | "world"Utility type Extract và Exclude tích hợp sẵn của TypeScript hoạt động chính xác theo cách này:
// Định nghĩa tích hợp sẵn:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type Numbers = Exclude<string | number | boolean, string>;
// number | booleanNgăn chặn Distribution
Đôi khi bạn không muốn distribution. Bọc cả hai phía trong tuple:
type IsNever<T> = [T] extends [never] ? true : false;
type A = IsNever<never>; // true
type B = IsNever<string>; // false
// Không có tuple wrapper, `never` phân phối thành `never`
type Broken<T> = T extends never ? true : false;
type C = Broken<never>; // never (không phải true!)2. Từ khóa infer
infer cho phép bạn trích xuất kiểu từ bên trong conditional type. Hãy nghĩ về nó như pattern matching cho kiểu.
Trích xuất kiểu trả về
type ReturnOf<T> = T extends (...args: unknown[]) => infer R ? R : never;
type A = ReturnOf<() => string>; // string
type B = ReturnOf<(x: number) => boolean>; // boolean
type C = ReturnOf<string>; // neverĐây chính xác là cách ReturnType<T> tích hợp sẵn hoạt động.
Trích xuất kiểu tham số
type FirstParam<T> = T extends (first: infer P, ...rest: unknown[]) => unknown
? P
: never;
type A = FirstParam<(name: string, age: number) => void>; // string
type B = FirstParam<() => void>; // neverTrích xuất từ Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number (đệ quy!)
type C = Awaited<string>; // string (không phải promise)Trích xuất từ Array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<[string, number]>; // string | numberVí dụ thực tế: Trích xuất Event Payload
type EventMap = {
"user:login": { userId: string; timestamp: Date };
"user:logout": { userId: string };
"order:created": { orderId: string; total: number };
};
type EventPayload<K extends keyof EventMap> = EventMap[K];
type LoginPayload = EventPayload<"user:login">;
// { userId: string; timestamp: Date }3. Mapped Type
Mapped type biến đổi mọi thuộc tính của kiểu hiện có. Bạn đã sử dụng các kiểu tích hợp sẵn như Partial<T> và Readonly<T> — đây là cách chúng hoạt động bên dưới.
Mapped Type cơ bản
// Làm tất cả thuộc tính optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Làm tất cả thuộc tính readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Làm tất cả thuộc tính required
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};Modifier -? loại bỏ tính optional. Tương tự, -readonly loại bỏ readonly.
Biến đổi giá trị thuộc tính
interface User {
name: string;
age: number;
email: string;
}
// Bọc mỗi thuộc tính trong hàm getter
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }Key Remapping với as
Bạn có thể đổi tên key trong quá trình mapping:
// Thêm prefix "on" cho tất cả key
type Listeners<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
value: T[K]
) => void;
};
type UserListeners = Listeners<User>;
// {
// onNameChange: (value: string) => void;
// onAgeChange: (value: number) => void;
// onEmailChange: (value: string) => void;
// }Lọc thuộc tính
Trả về never làm key để lọc bỏ thuộc tính:
// Chỉ giữ thuộc tính kiểu string
type StringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringProps<User>;
// { name: string; email: string }Ví dụ thực tế: Form Validation
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
type FormErrors<T> = {
[K in keyof T]?: string[];
};
type FormTouched<T> = {
[K in keyof T]?: boolean;
};
type FormState<T> = {
values: T;
errors: FormErrors<T>;
touched: FormTouched<T>;
isValid: boolean;
isSubmitting: boolean;
};
const loginState: FormState<LoginForm> = {
values: { email: "", password: "", rememberMe: false },
errors: { email: ["Email is required"] },
touched: { email: true },
isValid: false,
isSubmitting: false,
};4. Template Literal Type
Template literal type cho phép bạn xây dựng kiểu từ pattern chuỗi — cùng cú pháp template literal bạn dùng trong JavaScript, nhưng ở cấp kiểu.
Thao tác chuỗi cơ bản
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, World"; // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, World"; // Lỗi!
// Kết hợp với union để tạo tổ hợp
type Color = "red" | "green" | "blue";
type Opacity = "light" | "dark";
type Theme = `${Opacity}-${Color}`;
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"Kiểu thao tác chuỗi tích hợp sẵn
TypeScript cung cấp bốn kiểu thao tác chuỗi intrinsic:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"Pattern Matching với Template Literal
Kết hợp với infer để phân tích chuỗi ở cấp kiểu:
// Trích xuất path parameter từ route string
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"Ví dụ thực tế: Type-Safe Event Emitter
type EventName = `${string}:${string}`;
type SplitEvent<T extends string> =
T extends `${infer Domain}:${infer Action}`
? { domain: Domain; action: Action }
: never;
type Parsed = SplitEvent<"user:login">;
// { domain: "user"; action: "login" }
// Type-safe CSS property builder
type CSSUnit = "px" | "rem" | "em" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;
function setWidth(value: CSSValue): void {
console.log(`Setting width: ${value}`);
}
setWidth("100px"); // OK
setWidth("2.5rem"); // OK
// setWidth("100"); // Lỗi: không phải CSSValue hợp lệ5. Recursive Type
Recursive type tham chiếu đến chính nó — thiết yếu để mô hình hóa cấu trúc dữ liệu lồng nhau như cây, JSON và object lồng sâu.
Kiểu JSON
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// Kiểu này mô tả chính xác mọi giá trị JSON hợp lệ
const data: JSONValue = {
name: "Alice",
age: 30,
hobbies: ["reading", "coding"],
address: {
city: "Portland",
zip: "97201",
},
};Deep Readonly
type DeepReadonly<T> = T extends (infer E)[]
? ReadonlyArray<DeepReadonly<E>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
features: string[];
}
type ReadonlyConfig = DeepReadonly<Config>;
// Tất cả thuộc tính lồng nhau đều readonly — bao gồm phần tử arrayDeep Partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// Hữu ích cho ghi đè cấu hình
function mergeConfig(
base: Config,
overrides: DeepPartial<Config>
): Config {
return {
...base,
...overrides,
database: {
...base.database,
...overrides.database,
credentials: {
...base.database.credentials,
...overrides.database?.credentials,
},
},
features: overrides.features ?? base.features,
};
}
// Chỉ ghi đè những gì bạn cần
mergeConfig(defaultConfig, {
database: { port: 5433 },
});Flatten Nested Array
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string[]>; // string
type C = Flatten<boolean>; // booleanĐường dẫn Dot-Notation
Nhu cầu phổ biến: lấy tất cả đường dẫn dot-notation hợp lệ cho object lồng nhau:
type Paths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
interface Settings {
theme: {
mode: "light" | "dark";
colors: {
primary: string;
secondary: string;
};
};
notifications: {
email: boolean;
push: boolean;
};
}
type SettingPaths = Paths<Settings>;
// "theme" | "theme.mode" | "theme.colors" | "theme.colors.primary"
// | "theme.colors.secondary" | "notifications" | "notifications.email"
// | "notifications.push"6. Discriminated Union
Discriminated union (hay tagged union) là một trong những pattern kiểu thực tế nhất. Thuộc tính literal chung (discriminant) cho phép TypeScript thu hẹp kiểu tự động.
Pattern cơ bản
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}Kiểm tra Exhaustive
Đảm bảo bạn xử lý mọi variant với kỹ thuật never:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
// Nếu bạn thêm variant shape mới mà không xử lý,
// TypeScript sẽ báo lỗi ở đây tại compile time
}
}Kiểu Result (Xử lý lỗi)
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { ok: false, error: "Division by zero" };
}
return { ok: true, value: a / b };
}
const result = divide(10, 3);
if (result.ok) {
console.log(result.value); // TypeScript biết đây là number
} else {
console.error(result.error); // TypeScript biết đây là string
}Pattern API Response
type ApiResponse<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string; statusCode: number };
function renderUser(response: ApiResponse<User>) {
switch (response.status) {
case "loading":
return <Spinner />;
case "success":
return <UserCard user={response.data} />;
case "error":
return <ErrorMessage message={response.error} />;
}
}7. Branded Type
TypeScript sử dụng structural typing — nếu hai kiểu có cùng cấu trúc, chúng tương thích. Điều này có thể gây lỗi khi bạn muốn phân biệt giữa các giá trị khác nhau về mặt khái niệm.
Vấn đề
type UserId = string;
type OrderId = string;
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
const userId: UserId = "user_123";
const orderId: OrderId = "order_456";
// Biên dịch được nhưng SAI — bạn đang truyền order ID cho getUser!
getUser(orderId); // Không có lỗi! Cả hai đều chỉ là `string`Giải pháp: Branded Type
Thêm thuộc tính phantom chỉ tồn tại ở cấp kiểu:
// Brand utility
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
// Hàm khởi tạo
function createUserId(id: string): UserId {
// Thêm validation ở đây nếu cần
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId): void {
console.log(`Getting user: ${id}`);
}
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
getUser(userId); // OK
// getUser(orderId); // Lỗi: Type 'OrderId' is not assignable to type 'UserId'Branded Numeric Type
type Meters = Brand<number, "Meters">;
type Kilometers = Brand<number, "Kilometers">;
type Celsius = Brand<number, "Celsius">;
type Fahrenheit = Brand<number, "Fahrenheit">;
function metersToKilometers(m: Meters): Kilometers {
return (m / 1000) as Kilometers;
}
function celsiusToFahrenheit(c: Celsius): Fahrenheit {
return (c * 9 / 5 + 32) as Fahrenheit;
}
const distance = 5000 as Meters;
const km = metersToKilometers(distance); // OK
const temp = 100 as Celsius;
// metersToKilometers(temp); // Lỗi! Không thể truyền Celsius làm MetersChuỗi đã validate
type Email = Brand<string, "Email">;
type URL = Brand<string, "URL">;
function validateEmail(input: string): Email | null {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(input) ? (input as Email) : null;
}
function sendEmail(to: Email, subject: string): void {
// `to` được đảm bảo là email đã validate
console.log(`Sending "${subject}" to ${to}`);
}
const email = validateEmail("[email protected]");
if (email) {
sendEmail(email, "Welcome!"); // OK — đã validate
}
// sendEmail("not-validated", "Oops"); // Lỗi!8. Variadic Tuple Type
Variadic tuple type (TypeScript 4.0+) cho phép bạn làm việc với tuple có độ dài không xác định trong khi vẫn giữ thông tin kiểu.
Spread trong Tuple Type
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
type AB = Concat<[string, number], [boolean]>;
// [string, number, boolean]Typed Function Composition
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
type A = Head<[string, number, boolean]>; // string
type B = Tail<[string, number, boolean]>; // [number, boolean]
type C = Last<[string, number, boolean]>; // booleanVí dụ thực tế: Typed Middleware Pipeline
type Middleware<In, Out> = (input: In) => Out;
function pipe<A, B>(m1: Middleware<A, B>): Middleware<A, B>;
function pipe<A, B, C>(
m1: Middleware<A, B>,
m2: Middleware<B, C>
): Middleware<A, C>;
function pipe<A, B, C, D>(
m1: Middleware<A, B>,
m2: Middleware<B, C>,
m3: Middleware<C, D>
): Middleware<A, D>;
function pipe(...fns: Middleware<unknown, unknown>[]): Middleware<unknown, unknown> {
return (input) => fns.reduce((acc, fn) => fn(acc), input);
}
const transform = pipe(
(input: string) => input.length, // string → number
(length: number) => length > 5, // number → boolean
(isLong: boolean) => isLong ? "yes" : "no" // boolean → string
);
const result = transform("TypeScript"); // "yes"9. Const Assertion và satisfies
as const — Const Assertion
as const làm TypeScript suy luận kiểu hẹp nhất có thể:
// Không có as const
const colors = ["red", "green", "blue"];
// kiểu: string[]
// Có as const
const colors = ["red", "green", "blue"] as const;
// kiểu: readonly ["red", "green", "blue"]
// Không có as const
const config = { mode: "production", port: 3000 };
// kiểu: { mode: string; port: number }
// Có as const
const config = { mode: "production", port: 3000 } as const;
// kiểu: { readonly mode: "production"; readonly port: 3000 }Suy luận kiểu từ hằng số
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
const STATUS_MAP = {
active: "Active",
inactive: "Inactive",
pending: "Pending Review",
} as const;
type StatusKey = keyof typeof STATUS_MAP; // "active" | "inactive" | "pending"
type StatusLabel = (typeof STATUS_MAP)[StatusKey]; // "Active" | "Inactive" | "Pending Review"satisfies — Kiểm tra kiểu không mở rộng
Toán tử satisfies (TypeScript 4.9+) xác nhận giá trị khớp với kiểu mà không mất thông tin kiểu cụ thể:
type ColorMap = Record<string, string | number[]>;
// Dùng type annotation — mất chi tiết
const colorsAnnotated: ColorMap = {
red: "#ff0000",
green: [0, 255, 0],
};
colorsAnnotated.red.toUpperCase(); // Lỗi! Có thể là string | number[]
// Dùng satisfies — giữ chi tiết
const colorsSatisfied = {
red: "#ff0000",
green: [0, 255, 0],
} satisfies ColorMap;
colorsSatisfied.red.toUpperCase(); // OK! TypeScript biết đây là string
colorsSatisfied.green.map(x => x); // OK! TypeScript biết đây là number[]Kết hợp as const và satisfies
type Route = {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
};
const routes = {
getUsers: { path: "/users", method: "GET" },
createUser: { path: "/users", method: "POST" },
getUser: { path: "/users/:id", method: "GET" },
} as const satisfies Record<string, Route>;
// Truy cập type-safe với kiểu literal chính xác
type GetUsersPath = (typeof routes)["getUsers"]["path"]; // "/users"
type GetUsersMethod = (typeof routes)["getUsers"]["method"]; // "GET"10. Pattern thực tế: API Client Type-Safe
Hãy kết hợp tất cả vào một ví dụ thực tế — API client type-safe suy luận kiểu request và response từ định nghĩa route.
Bước 1: Định nghĩa API Schema
interface ApiSchema {
"GET /users": {
response: User[];
query: { page?: number; limit?: number };
};
"GET /users/:id": {
response: User;
params: { id: string };
};
"POST /users": {
response: User;
body: { name: string; email: string };
};
"PUT /users/:id": {
response: User;
params: { id: string };
body: Partial<{ name: string; email: string }>;
};
"DELETE /users/:id": {
response: void;
params: { id: string };
};
}
interface User {
id: string;
name: string;
email: string;
createdAt: string;
}Bước 2: Trích xuất kiểu từ Route
// Trích xuất HTTP method từ route key
type ExtractMethod<T extends string> =
T extends `${infer M} ${string}` ? M : never;
// Trích xuất path từ route key
type ExtractPath<T extends string> =
T extends `${string} ${infer P}` ? P : never;
// Trích xuất path parameter
type ExtractPathParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractPathParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
// Kiểm thử
type Method = ExtractMethod<"GET /users/:id">; // "GET"
type Path = ExtractPath<"GET /users/:id">; // "/users/:id"
type Params = ExtractPathParams<"/users/:id">; // "id"Bước 3: Xây dựng Client
type ApiEndpoint = keyof ApiSchema;
type RequestOptions<E extends ApiEndpoint> = {
params: "params" extends keyof ApiSchema[E]
? ApiSchema[E]["params"]
: never;
query: "query" extends keyof ApiSchema[E]
? ApiSchema[E]["query"]
: never;
body: "body" extends keyof ApiSchema[E]
? ApiSchema[E]["body"]
: never;
};
// Loại bỏ trường `never` để caller không cần truyền chúng
type CleanOptions<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
};
type FetchOptions<E extends ApiEndpoint> = CleanOptions<RequestOptions<E>>;
async function apiClient<E extends ApiEndpoint>(
endpoint: E,
...args: keyof FetchOptions<E> extends never
? []
: [options: FetchOptions<E>]
): Promise<ApiSchema[E]["response"]> {
const [method, path] = (endpoint as string).split(" ");
let url = path;
const options = args[0] as Record<string, unknown> | undefined;
// Thay thế path params
if (options && "params" in options) {
const params = options.params as Record<string, string>;
for (const [key, value] of Object.entries(params)) {
url = url.replace(`:${key}`, value);
}
}
// Thêm query params
if (options && "query" in options) {
const query = options.query as Record<string, string>;
const searchParams = new URLSearchParams(query);
url += `?${searchParams.toString()}`;
}
const response = await fetch(`/api${url}`, {
method,
headers: { "Content-Type": "application/json" },
body: options && "body" in options
? JSON.stringify(options.body)
: undefined,
});
return response.json();
}Bước 4: Sử dụng Client
// GET /users — query params tùy chọn
const users = await apiClient("GET /users", {
query: { page: 1, limit: 10 },
});
// users: User[]
// GET /users/:id — yêu cầu params
const user = await apiClient("GET /users/:id", {
params: { id: "123" },
});
// user: User
// POST /users — yêu cầu body
const newUser = await apiClient("POST /users", {
body: { name: "Alice", email: "[email protected]" },
});
// newUser: User
// DELETE /users/:id — yêu cầu params
await apiClient("DELETE /users/:id", {
params: { id: "123" },
});
// void
// Lỗi kiểu được bắt tại compile time:
// apiClient("GET /users/:id"); // Lỗi: thiếu params
// apiClient("POST /users", { body: { name: 123 } }); // Lỗi: name phải là string
// apiClient("PATCH /users"); // Lỗi: endpoint không tồn tạiMỗi route đều được định kiểu đầy đủ — params, query, body và response. Thêm route mới vào ApiSchema và client tự động biết cách gọi nó.
Lỗi thường gặp
1. Kiểu quá phức tạp
Không phải mọi vấn đề đều cần kiểu nâng cao. Nếu một kiểu mất hơn 10 giây để hiểu, hãy cân nhắc đơn giản hóa:
// Quá phức tạp — khó đọc và bảo trì
type DeepMerge<A, B> = {
[K in keyof A | keyof B]: K extends keyof A & keyof B
? A[K] extends object
? B[K] extends object
? DeepMerge<A[K], B[K]>
: B[K]
: B[K]
: K extends keyof B
? B[K]
: K extends keyof A
? A[K]
: never;
};
// Thường đơn giản hơn: dùng hàm runtime với kiểu trả về hợp lý
function deepMerge<T extends object>(base: T, overrides: DeepPartial<T>): T {
// ... implementation
}2. Tham chiếu kiểu vòng tròn
TypeScript có giới hạn độ sâu đệ quy. Đệ quy sâu có thể gây:
// Có thể gây lỗi "Type instantiation is excessively deep"
type DeepPath<T, Depth extends number[] = []> =
Depth["length"] extends 10 ? never : // Thêm giới hạn độ sâu
T extends object
? { [K in keyof T & string]:
| K
| `${K}.${DeepPath<T[K], [...Depth, 0]>}` }[keyof T & string]
: never;3. Lạm dụng as
Type assertion (as) bỏ qua type checker. Mỗi as là một lỗi tiềm ẩn:
// Tránh
const data = fetchData() as User;
// Ưu tiên: validate tại runtime
function isUser(data: unknown): data is User {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
"name" in data &&
"email" in data
);
}
const data = fetchData();
if (isUser(data)) {
// TypeScript biết data là User
console.log(data.name);
}Tổng kết và điểm chính
✅ Conditional type: If/else ở cấp kiểu với extends — xử lý union distributive
✅ Từ khóa infer: Trích xuất kiểu từ cấu trúc phức tạp — return type, tham số, promise
✅ Mapped type: Biến đổi tất cả thuộc tính — với key remapping và lọc
✅ Template literal type: Thao tác chuỗi ở cấp kiểu — phân tích route, xây dựng event name
✅ Recursive type: Mô hình hóa dữ liệu lồng nhau — JSON, deep readonly, đường dẫn dot-notation
✅ Discriminated union: Switch/case type-safe với exhaustive checking
✅ Branded type: Nominal typing ngăn nhầm lẫn ID/đơn vị
✅ Variadic tuple: Làm việc với tuple có độ dài không xác định
✅ as const + satisfies: Thu hẹp hằng số trong khi validate với kiểu
Khi nào dùng kiểu nâng cao
| Pattern | Sử dụng khi |
|---|---|
| Conditional type | Xây dựng utility kiểu, lọc union |
infer | Trích xuất kiểu từ hàm, promise, array |
| Mapped type | Biến đổi API interface, form type |
| Template literal | Route typing, event system, CSS-in-JS |
| Branded type | ID, đơn vị, chuỗi đã validate |
| Discriminated union | State machine, API response, action type |
Bước tiếp theo
Bây giờ bạn đã nắm vững kiểu nâng cao, hãy áp dụng chúng vào framework thực tế:
- Deep Dive: React với TypeScript → — Áp dụng kiểu nâng cao vào component pattern
- Deep Dive: Phát triển API Node.js → — Xây dựng backend type-safe
Tài liệu tham khảo
- TypeScript Handbook: Advanced Types
- Type Challenges — Thực hành lập trình ở cấp kiểu
- Matt Pocock's Total TypeScript — Hướng dẫn nâng cao xuất sắc
Series: Lộ trình TypeScript Full-Stack Trước: Giai đoạn 4: Tích hợp Full-Stack Tiếp: Deep Dive: React với TypeScript
Happy coding! 🚀
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.