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
TypeScript
draftedStructural 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.- 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[].
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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~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~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
TenantIdvớiUserId. - 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ệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Structural typing | Hai type tương thích nếu shape tương thích, không quan tâm tên | type A = {id: string} nhận được object {id, name} qua biến |
unknown vs any | any tắt type check; unknown bắt phải narrow trước khi dùng | JSON.parse nên được coi là unknown |
never | Tập rỗng — giá trị không thể tồn tại | Exhaustive check trong switch |
| Narrowing | TS thu hẹp type theo control flow | if (typeof x === "string") |
| Discriminated union | Union có field literal chung để phân nhánh | {status: "ok", data} | {status: "err", error} |
| Generics + constraint | Tham 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 union | T extends string ? "s" : "o" |
| Mapped type | "for-loop" qua key | { [K in keyof T]?: T[K] } = Partial<T> |
satisfies | Kiểm tra value khớp type mà không làm mất literal type đã infer | const cfg = {...} satisfies Config |
| Branded type | Giả lập nominal typing bằng phantom property | type 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 --noEmitriêng trong CI. - Node (v22.18+/v23.6+) chạy được
.tstrự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 — xemerasableSyntaxOnly. (verify theo version Node)
Narrowing theo control flow:
Trade-offs
| Lựa chọn | Ưu | Nhược | Dùng khi |
|---|---|---|---|
interface | Declaration merging, error message gọn, extends báo lỗi xung đột sớm | Không biểu diễn union/tuple/conditional | Object contract, public API, module augmentation |
type alias | Union, intersection, mapped, conditional | Không merge; & xung đột → never âm thầm | Logic ở tầng type, state machine |
enum | Có 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 cast | Nhanh | Nói dối compiler, không kiểm tra runtime | Chỉ khi bạn biết hơn compiler (DOM, test) |
zod schema → z.infer | Một nguồn sự thật cho runtime + type | Chi phí parse, bundle size | Mọi boundary không tin cậy |
| Transpile-only (swc/esbuild) | Build rất nhanh | Không check type, isolatedModules hạn chế | Dev loop; CI vẫn cần tsc --noEmit |
Pitfalls
- Tin
as Usercho response API → crash runtime ở chỗ xa nguồn lỗi. obj[key]với index signature trảTchứ không phảiT | undefinednếu không bậtnoUncheckedIndexedAccess.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 choAnimal[]rồipush(cat). - Method shorthand trong interface bị bivariant kể cả khi bật
strictFunctionTypes. useDefineForClassFields(mặc địnhtruekhitarget >= ES2022) đổi semantics class field — có thể ghi đè giá trị do base class/decorator set.- Conditional type distributive trên union —
IsString<string | number>raboolean, không phảifalse.
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(...) }. satisfiesgiữ literal type; annotation: Twiden vềT;as Tbỏ qua check.- Tắt distributive:
[T] extends [U] ? X : Y. Checknever:[T] extends [never]. inferchỉ dùng trong nhánhextendscủa conditional type.- Version cần nhớ (verify):
satisfies4.9 ·consttype params &verbatimModuleSyntax5.0 ·using5.2 ·NoInfer5.4 · inferred type predicates &isolatedDeclarations5.5 ·erasableSyntaxOnly5.8 · TS 6.0 đổi default (strict: true,module: esnext,types: []) · TS 7.0 = compiler viết lại bằng Go.
References
Interview Questions
Showing 46 / 46
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[]>;
}