Skip to content
DevRecall
← Roadmap
04

TypeScript

drafted

Structural typing, generics, narrowing, conditional/mapped types, satisfies, type erasure & runtime validation, tsconfig và các gotcha khi scale codebase.

0/46 rated · mastery 0%

Bài học

11 bài · ~130 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~13 phút· 4 câuverify

    Nền tảng type system: type là tập hợp, structural typing, any/unknown/never

    TypeScript là type checker chạy lúc compile; type là tập giá trị; assignability giữa any, unknown, never; structural typing, excess property check và vì sao Object.keys trả string[].

  2. 02~12 phút· 5 câuverify

    type vs interface, literal types, as const, enum và optional property

    Khi nào dùng type hay interface, declaration merging để augment Express Request, literal widening, as const so với Readonly và Object.freeze, enum so với union literal, và ?: so với | undefined dưới exactOptionalPropertyTypes.

  3. 03~12 phút· 4 câuverify

    Narrowing, type guard và discriminated union

    Control flow analysis thu hẹp type ra sao, typeof/instanceof/in/discriminant, type predicate so với assertion function, exhaustive check bằng never, inferred type predicates (TS 5.5) và vì sao type guard viết tay là chỗ unsound.

  4. 04~12 phút· 4 câuverify

    Generics, inference, overloads và satisfies

    Type parameter và constraint, keyof và indexed access, inference từ argument, overload so với union và generic, const type parameter, NoInfer, và ba cách gắn type cho giá trị: annotation, as, satisfies.

  5. 05~11 phút· 4 câuverify

    Type-level programming: utility types, mapped, conditional, infer và template literal

    Tự viết Partial/Pick/Omit, mapped type với key remapping, conditional type và tính distributive trên union, infer để pattern-match, template literal type để parse route params, và giới hạn độ phức tạp của compiler.

  6. 06~10 phút· 4 câuverify

    Soundness và variance: chỗ nào TypeScript cố ý cho phép sai

    Soundness là gì, covariance/contravariance/bivariance, strictFunctionTypes và method shorthand, array covariant, index access và noUncheckedIndexedAccess, và bản đồ các nguồn unsoundness gây crash dưới strict.

  7. 07~12 phút· 6 câuverify

    Type erasure và runtime validation: parse ở boundary, contract giữa các service

    Vì sao req.body as Dto không kiểm tra gì, parse don't validate với zod, tiền và số lớn qua JSON, payload của provider bên ngoài, event contract Kafka có version, document Elasticsearch, và chia sẻ type giữa Next.js và API trong monorepo.

  8. 08~11 phút· 5 câuverify

    Domain modeling: branded types, lỗi có type, tenant scoping và RBAC

    Giả lập nominal typing bằng brand, nơi được phép brand, Result type so với throw và error class, data-access layer bắt buộc tenant context, và mô hình permission bằng as const + satisfies để typo làm fail build.

  9. 09~12 phút· 4 câuverify

    tsconfig cho backend: strict, module resolution và declaration files

    strict bật những gì và nên thêm flag nào, TS 6.0 đổi default ra sao, moduleResolution nodenext so với bundler và vì sao cần đuôi .js, cách viết và publish .d.ts cho package nội bộ, và cái giá của skipLibCheck.

  10. 10~12 phút· 4 câuverify

    Từ .ts ra runtime: type stripping, import type, class fields và decorators

    tsc, esbuild/swc và Node type stripping khác nhau thế nào, erasable syntax, import type và verbatimModuleSyntax, useDefineForClassFields khi nâng target ES2022, và legacy decorators (NestJS) so với TC39 decorators.

  11. 11~13 phút· 4 câuverify

    TypeScript ở quy mô lớn: tốc độ type-check, nâng version, escape hatch và review code AI

    Đo và tăng tốc tsc trên monorepo (extendedDiagnostics, trace, project references, isolatedDeclarations, TS 7 native), lộ trình nâng TS 5 → 6 → 7 cho nhiều package, chính sách any/ts-ignore trong team, và các lỗi type hay gặp trong code do AI sinh.

Tóm tắt & cheat sheet của track

TL;DR

  • TypeScript là type checker chạy lúc compile; lúc runtime chỉ còn JavaScript (type erasure). Mọi dữ liệu đi qua boundary (HTTP body, JSON, Kafka message, env) phải validate runtime (zod/valibot) rồi mới tin type.
  • Hệ thống type là structural (so sánh theo shape), không phải nominal — cần branded types nếu muốn phân biệt TenantId với UserId.
  • Senior interview hỏi về narrowing, discriminated union, generics có constraint, conditional/mapped types, variance và chỗ nào TS unsound (as, any, !, index access, type guard sai).
  • Tooling quan trọng không kém: strict, noUncheckedIndexedAccess, verbatimModuleSyntax, moduleResolution, và tốc độ type-check trên monorepo lớn (TS 7 native compiler — verify version khi đọc).

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
Structural typingHai type tương thích nếu shape tương thích, không quan tâm têntype A = {id: string} nhận được object {id, name} qua biến
unknown vs anyany tắt type check; unknown bắt phải narrow trước khi dùngJSON.parse nên được coi là unknown
neverTập rỗng — giá trị không thể tồn tạiExhaustive check trong switch
NarrowingTS thu hẹp type theo control flowif (typeof x === "string")
Discriminated unionUnion có field literal chung để phân nhánh{status: "ok", data} | {status: "err", error}
Generics + constraintTham số hoá type, giới hạn bằng extends<T, K extends keyof T>(o: T, k: K) => T[K]
Conditional type"if/else" ở tầng type, distributive trên unionT extends string ? "s" : "o"
Mapped type"for-loop" qua key{ [K in keyof T]?: T[K] } = Partial<T>
satisfiesKiểm tra value khớp type mà không làm mất literal type đã inferconst cfg = {...} satisfies Config
Branded typeGiả lập nominal typing bằng phantom propertytype TenantId = string & { __brand: "TenantId" }

Cơ chế hoạt động

TypeScript chỉ tồn tại ở build time. Có hai con đường từ .ts ra runtime, và cả hai đều xoá type:

  • Transpiler kiểu esbuild/swc/Node type stripping không type-check — phải chạy tsc --noEmit riêng trong CI.
  • Node (v22.18+/v23.6+) chạy được .ts trực tiếp bằng type stripping, nhưng không hỗ trợ syntax cần transform (enum, namespace có runtime code, parameter properties, decorators) và bỏ qua tsconfig — xem erasableSyntaxOnly. (verify theo version Node)

Narrowing theo control flow:

Trade-offs

Lựa chọnƯuNhượcDùng khi
interfaceDeclaration merging, error message gọn, extends báo lỗi xung đột sớmKhông biểu diễn union/tuple/conditionalObject contract, public API, module augmentation
type aliasUnion, intersection, mapped, conditionalKhông merge; & xung đột → never âm thầmLogic ở tầng type, state machine
enumCó runtime object, reverse mapping (numeric)Sinh code, không erasable, numeric enum dễ nhận số bất kỳHiếm; ưu tiên union literal / as const object
as castNhanhNói dối compiler, không kiểm tra runtimeChỉ khi bạn biết hơn compiler (DOM, test)
zod schema → z.inferMột nguồn sự thật cho runtime + typeChi phí parse, bundle sizeMọi boundary không tin cậy
Transpile-only (swc/esbuild)Build rất nhanhKhông check type, isolatedModules hạn chếDev loop; CI vẫn cần tsc --noEmit

Pitfalls

  • Tin as User cho response API → crash runtime ở chỗ xa nguồn lỗi.
  • obj[key] với index signature trả T chứ không phải T | undefined nếu không bật noUncheckedIndexedAccess.
  • Object.keys(obj) trả string[] — đúng thiết kế vì structural typing cho phép thừa property.
  • Type guard tự viết (x is T) không được compiler kiểm tra — viết sai là unsound.
  • Array là covariant (unsound): Dog[] gán được cho Animal[] rồi push(cat).
  • Method shorthand trong interface bị bivariant kể cả khi bật strictFunctionTypes.
  • useDefineForClassFields (mặc định true khi target >= ES2022) đổi semantics class field — có thể ghi đè giá trị do base class/decorator set.
  • Conditional type distributive trên union — IsString<string | number> ra boolean, không phải false.

Cheat sheet

  • Assignability: mọi thứ → unknown ✅; any → mọi thứ trừ never ✅; unknown → type cụ thể ❌ (phải narrow); never → mọi thứ ✅; không gì → never, kể cả any (TS2322: Type 'any' is not assignable to type 'never').
  • Exhaustive: default: { const _x: never = value; throw new Error(...) }.
  • satisfies giữ literal type; annotation : T widen về T; as T bỏ qua check.
  • Tắt distributive: [T] extends [U] ? X : Y. Check never: [T] extends [never].
  • infer chỉ dùng trong nhánh extends của conditional type.
  • Version cần nhớ (verify): satisfies 4.9 · const type params & verbatimModuleSyntax 5.0 · using 5.2 · NoInfer 5.4 · inferred type predicates & isolatedDeclarations 5.5 · erasableSyntaxOnly 5.8 · TS 6.0 đổi default (strict: true, module: esnext, types: []) · TS 7.0 = compiler viết lại bằng Go.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 46 / 46

let a: any; let u: unknown; let s: string;
s = a; // ok
u = a; // ok
a = u; // ok
s = u; // error: unknown is not assignable to string
if (typeof u === "string") s = u; // ok after narrowing
type Point = { x: number; y: number };
const p3 = { x: 1, y: 2, z: 3 };
const p: Point = p3;            // ok (structural)
const q: Point = { x: 1, y: 2, z: 3 }; // error: excess property 'z'
interface Patch { nickname?: string }
const a: Patch = {};                      // ok
const b: Patch = { nickname: undefined }; // error only with exactOptionalPropertyTypes
const ROLES = ["admin", "staff", "customer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "staff" | "customer"
const cfg = { retries: 3 } as const; // { readonly retries: 3 }
const CreateOrder = z.object({ sku: z.string(), qty: z.number().int().positive() });
type CreateOrder = z.infer<typeof CreateOrder>;
app.post("/orders", (req, res) => {
  const body = CreateOrder.parse(req.body); // throws ZodError -> 400
  // body is CreateOrder from here on
});
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type UserPreview = MyPick<User, "id" | "name">;
function assertDefined<T>(v: T, msg: string): asserts v is NonNullable<T> {
  if (v == null) throw new Error(msg);
}
const user = await repo.find(id);
assertDefined(user, "user not found");
user.email; // User, not User | null
type PaymentResult =
  | { status: "succeeded"; chargeId: string }
  | { status: "requires_action"; redirectUrl: string }
  | { status: "failed"; reason: string };

function handle(r: PaymentResult) {
  switch (r.status) {
    case "succeeded": return ok(r.chargeId);
    case "requires_action": return redirect(r.redirectUrl);
    case "failed": return fail(r.reason);
    default: { const _never: never = r; throw new Error("unhandled"); }
  }
}
type Route = { path: string; auth: boolean };
const routes = {
  home: { path: "/", auth: false },
  orders: { path: "/orders", auth: true },
} satisfies Record<string, Route>;
routes.orders.path; // ok, key "orders" is known
routes.nope;        // error — would be allowed with ': Record<string, Route>'
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((i) => i[key]);
}
const emails = pluck(users, "email"); // string[]
pluck(users, "emali"); // error
interface UpdateProfile { name: string }
const body = { name: "A", role: "admin" };
const dto: UpdateProfile = body; // compiles
await db.user.update({ where: { id }, data: { ...dto } }); // role leaks through at runtime
class Animal { name = "" }
class Dog extends Animal { bark() {} }
interface A { handle(x: Animal): void }      // method shorthand: bivariant
interface B { handle: (x: Animal) => void }  // property: contravariant under strict
const onlyDogs = (d: Dog) => d.bark();
const a: A = { handle: onlyDogs }; // compiles (unsound): a.handle(new Animal()) crashes
const b: B = { handle: onlyDogs }; // error
type IsString<T> = T extends string ? true : false;
type A = IsString<string | number>;  // ?
type B = IsString<never>;            // ?
type ToArray<T> = T extends unknown ? T[] : never;
type C = ToArray<string | number>;   // ?
type IsStringStrict<T> = [T] extends [string] ? true : false;
type D = IsStringStrict<string | number>; // ?
type Events = { orderCreated: { id: string }; orderPaid: { id: string; amount: number } };
type Handlers = {
  [K in keyof Events as `on${Capitalize<string & K>}`]: (payload: Events[K]) => void;
};
// { onOrderCreated: (p: {id: string}) => void; onOrderPaid: (p: {...}) => void }
type Params<P extends string> =
  P extends `${string}:${infer Name}/${infer Rest}`
    ? { [K in Name | keyof Params<`/${Rest}`>]: string }
    : P extends `${string}:${infer Name}`
      ? { [K in Name]: string }
      : {};
type X = Params<"/tenants/:tenantId/orders/:orderId">;
// { tenantId: string; orderId: string }
export const OrderStatus = { Pending: "pending", Paid: "paid", Shipped: "shipped" } as const;
export type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
const s: OrderStatus = "paid"; // ok, works with JSON from DB
// src/types/express.d.ts
import type { AuthUser } from "../auth/types";
declare global {
  namespace Express {
    interface Request { user?: AuthUser; tenantId?: string }
  }
}
export {};
const cache: Record<string, User> = {};
function email(id: string) {
  return cache[id].email; // compiles; TypeError at runtime on a miss
}
function find(id: string): Promise<User | null>;
function find(ids: string[]): Promise<User[]>;
function find(x: string | string[]) { /* impl */ return null as any; }
declare const v: string | string[];
find(v); // error: no overload matches a union argument
function createFSM<S extends string>(states: S[], initial: NoInfer<S>) { /* ... */ }
createFSM(["idle", "loading"], "idle");   // ok
createFSM(["idle", "loading"], "crashed"); // error (without NoInfer, S would widen)
interface User { id: string; email: string }
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null && "id" in x;
}
const payload: unknown = await res.json();
if (isUser(payload)) {
  sendMail(payload.email.toLowerCase());
}
type Invoice = { id: string; total: number };
async function getInvoice(id: string): Promise<Invoice> {
  const r = await fetch(`/api/invoices/${id}`);
  return (await r.json()) as Invoice;
}
const inv = await getInvoice("1");
render(inv.total + 100); // "1000100"
const ids: (string | undefined)[] = ["a", undefined, "b"];
const a = ids.filter(Boolean);                 // (string | undefined)[]
const b = ids.filter((x) => x !== undefined);  // string[] on TS >= 5.5
const c = ids.filter((x): x is string => !!x); // string[] on any version
class Base {
  constructor(data: Record<string, unknown>) { Object.assign(this, data); }
}
class Order extends Base {
  id!: string;
  total!: number;
}
new Order({ id: "o1", total: 10 }).id; // undefined after the upgrade
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
type TenantId = Brand<string, "TenantId">;
type ProductId = Brand<string, "ProductId">;
function getProduct(tenant: TenantId, id: ProductId) { /* ... */ }
getProduct(productId, tenantId); // compile error — swapped
class Animal { name = "" }
class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs;
animals.push(new Cat());
dogs[1].bark(); // TypeError at runtime
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
type CheckoutError = { kind: "OUT_OF_STOCK"; sku: string } | { kind: "PRICE_CHANGED" };
async function checkout(cart: Cart): Promise<Result<Order, CheckoutError>> { /* ... */ }
type TenantScoped<T> = { readonly tenantId: TenantId } & T;
interface OrderRepo {
  findById(ctx: TenantCtx, id: OrderId): Promise<Order | null>;
  list(ctx: TenantCtx, q: OrderQuery): Promise<Order[]>;
}