jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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ĩaQuy tắc production
unknownCó giá trị nhưng chưa có bằng chứngDùng ở boundary, narrow trước khi đọc
neverKhông thể có giá trịExhaustiveness và nhánh bất khả thi
anyTắt kiểm tra và lan truyềnChỉ cô lập trong adapter legacy
objectBất kỳ non-primitiveHiế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?
  • undefined là 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

  1. Tìm mọi anyas trong một feature; phân loại: boundary hợp lệ, workaround, hay bug bị che.
  2. Brand hai ID dễ tráo và chỉ cho phép tạo qua parser.
  3. Đổi input chỉ-đọc từ T[] sang readonly T[]; ghi lại mutation nào bị compiler chặn.
  4. 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.

Đọc tiếp