TypeScript Production · Phần 1 — Soundness, Structural Typing & Variance
Hiểu TypeScript thực sự chứng minh gì: structural typing, excess-property check, any/unknown/never, variance, array covariance và các lỗ hổng soundness cần cô lập.
Bạn không lên senior bằng cách thuộc thêm utility type. Bạn lên senior khi biết compiler đang bảo đảm điều gì, điều gì chỉ là giả định, và đặt validation đúng nơi giả định gặp dữ liệu thật.
Mental model: tập giá trị, không phải nhãn
type UserId = string;
type OrderId = string;
declare function loadUser(id: UserId): void;
const orderId: OrderId = 'ord_1';
loadUser(orderId); // hợp lệ: hai type có cùng cấu trúc
TypeScript là structural type system. Hai type tương thích khi cấu trúc tương thích, không vì chúng có cùng tên. Điều này rất tiện cho composition, nhưng domain primitive dễ bị tráo.
declare const userIdBrand: unique symbol;
type UserId = string & { readonly [userIdBrand]: 'UserId' };
function parseUserId(raw: string): UserId {
if (!raw.startsWith('usr_')) throw new Error('Invalid UserId');
return raw as UserId; // assertion được cô lập sau runtime check
}
Brand không tự validate. Nó chỉ lưu bằng chứng rằng validation đã chạy. Assertion hợp lý ở một constructor hẹp; assertion rải khắp business code là nợ kỹ thuật.
Assignability và excess-property check
type User = { id: string; name: string };
function save(user: User) {}
save({ id: '1', name: 'An', admin: true }); // lỗi trên object literal
const payload = { id: '1', name: 'An', admin: true };
save(payload); // hợp lệ: payload có ít nhất cấu trúc User
Excess-property check là heuristic bắt typo trên literal, không phải “exact object type”. Nếu security phụ thuộc việc cấm field thừa, hãy parse/strip/reject ở runtime.
Bốn điểm trên bản đồ rủi ro
| Type | Ý nghĩa | Quy tắc production |
|---|---|---|
unknown | Có giá trị nhưng chưa có bằng chứng | Dùng ở boundary, narrow trước khi đọc |
never | Không thể có giá trị | Exhaustiveness và nhánh bất khả thi |
any | Tắt kiểm tra và lan truyền | Chỉ cô lập trong adapter legacy |
object | Bất kỳ non-primitive | Hiếm khi là contract đủ tốt |
function assertNever(value: never): never {
throw new Error(`Unreachable: ${JSON.stringify(value)}`);
}
function message(value: unknown): string {
if (value instanceof Error) return value.message;
if (typeof value === 'string') return value;
return 'Unknown error';
}
unknown bắt người dùng tạo bằng chứng. any làm compiler ngừng hỏi. Đây là khác biệt kiến trúc, không chỉ style.
Variance: nơi callback thường bị hiểu sai
type Animal = { name: string };
type Dog = Animal & { bark(): void };
type Handler<T> = (value: T) => void;
const animalHandler: Handler<Animal> = (animal) => console.log(animal.name);
const dogHandler: Handler<Dog> = animalHandler; // an toàn
Một handler xử lý được mọi Animal chắc chắn xử lý được Dog. Chiều ngược lại không an toàn vì caller có thể truyền Cat. Với strictFunctionTypes, tham số function property được kiểm tra contravariant; method có một số ngoại lệ tương thích lịch sử. Khi thiết kế public API, ưu tiên function property nếu variance quan trọng.
Array mutable là lỗ hổng soundness kinh điển:
function addAnimal(animals: Animal[]) {
animals.push({ name: 'cat' });
}
const dogs: Dog[] = [{ name: 'Milo', bark() {} }];
addAnimal(dogs);
dogs[1].bark(); // compile được, runtime crash
Nếu chỉ đọc, nhận readonly Animal[]. Nếu cần thay đổi, tạo collection mới hoặc nhận đúng invariant abstraction.
Checklist review type contract
- Dữ liệu này do compiler tạo hay đến từ network, storage, environment, DOM?
- Assertion nào tạo “bằng chứng” và runtime check tương ứng ở đâu?
- Generic/container có mutable không? Nếu có, variance còn an toàn không?
undefinedlà trạng thái hợp lệ hay hậu quả truy cập sai?- Public type có mô tả domain hay chỉ mô tả shape tình cờ hôm nay?
Lab
- Tìm mọi
anyvàastrong một feature; phân loại: boundary hợp lệ, workaround, hay bug bị che. - Brand hai ID dễ tráo và chỉ cho phép tạo qua parser.
- Đổi input chỉ-đọc từ
T[]sangreadonly T[]; ghi lại mutation nào bị compiler chặn. - Viết một test chứng minh excess-property check không tạo exact type.
Done khi: business code không cần assertion để sử dụng dữ liệu từ boundary; mọi assertion còn lại nằm trong adapter nhỏ, có test runtime.