Type System cơ bản
TypeScript 6.0 & 7.0 — Những thay đổi quan trọng
strict: true là mặc định (TS 6.0+)
Từ TS 6.0, tất cả dự án không khai báo tường minh strict trong tsconfig.json sẽ tự động nhận strict: true. Điều này bao gồm toàn bộ 8 cờ con:
noImplicitAny— cấmanyngầm địnhstrictNullChecks—null/undefinedlà type riêngstrictFunctionTypes— kiểm tra function type chặt hơnstrictBindCallApply— kiểm tra.bind,.call,.applystrictPropertyInitialization— property class phải được khởi tạoalwaysStrict— emit"use strict"useUnknownInCatchVariables—catch(e)→e: unknownnoImplicitThis— cấmthisngầm định làany
"strict": true tường minh, sau khi nâng cấp TS 6.0 có thể gặp hàng loạt lỗi type mới. Luôn chạy tsc --noEmit để kiểm tra trước khi nâng cấp.types: [] là mặc định (TS 6.0+)
Trước đây TS tự động quét tất cả package @types/* trong node_modules. Từ TS 6.0, mặc định không còn auto-discovery nữa — phải khai báo tường minh:
// tsconfig.json
{
"compilerOptions": {
"types": ["node"] // nếu cần process, fs, Buffer...
}
}
process.env, describe, expect bị lỗi — tại sao?""Vì TS 6.0 mặc định
types: [], không còn auto-discovery@types/*. Phải thêm"types": ["node"](choprocess) hoặc"types": ["jest"](chodescribe,expect) vàotsconfig.json."
stableTypeOrdering: true — bắt buộc từ TS 7.0
- Thứ tự type được đảm bảo deterministic (xác định) giữa các lần chạy
- Cần thiết cho parallel type-checking — các worker khác nhau phải sinh ra kết quả giống hệt nhau
- Từ TS 7.0, không thể tắt tùy chọn này
- Có thể làm lộ ra các lỗi type tinh vi trước đây bị ẩn do phụ thuộc vào thứ tự type không xác định
stableTypeOrdering làm lộ bug ẩn trong generic phức tạp, conditional type, hoặc type inference phụ thuộc thứ tự.1. Primitive types
let s: string = "hello";
let n: number = 42; // không có int/float — số JS là double 64-bit
let b: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("x");
let u: undefined = undefined;
let nu: null = null;
let v: void; // chỉ cho return của function không return gì
let never_: never; // không bao giờ có giá trị (function throw, infinite loop)
null và undefined khác nhau ra sao trong TS?""Cùng là 'không có giá trị' nhưng khác semantic:
undefined= 'chưa được gán',null= 'có ý định gán = không gì'. VớistrictNullChecksbật, 2 type này khác nhau, phải khai báostring | nullhoặcstring | undefinedrõ ràng."
2. Object, array, tuple
// Object
let user: { id: number; name: string; age?: number } = {
id: 1, name: "Alice"
};
// Array
let nums: number[] = [1, 2, 3];
let nums2: Array<number> = [1, 2, 3]; // generic form, tương đương
// Tuple — fixed length + position-typed
let pair: [string, number] = ["a", 1];
let triple: [string, number, boolean] = ["a", 1, true];
// Rest in tuple
let log: [Date, ...string[]] = [new Date(), "info", "user logged in"];
// Readonly array
let ro: readonly number[] = [1, 2, 3];
ro.push(4); // ❌ error
let ro2: ReadonlyArray<number> = [1, 2, 3];
Map / WeakMap getOrInsert (TS 6.0)
const map = new Map<string, number>();
const val = map.getOrInsert("key", () => 42); // number — type-safe upsert
getOrInsert(key, factory) — nếu key tồn tại thì trả về value, nếu chưa thì gọi factory() để tạo value mới và insert. Type-safe hơn pattern if (!map.has(k)) map.set(k, v) vì TS biết chính xác return type.3. Literal types & Union
// Literal type — chỉ nhận giá trị cụ thể
let status: "active" | "inactive" | "pending";
status = "active"; // OK
status = "deleted"; // ❌ error
let dice: 1 | 2 | 3 | 4 | 5 | 6;
// Union — nhiều type
type Id = string | number;
function getUser(id: Id) { ... }
// Discriminated union (tagged union) — quan trọng
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2; // narrowed → có radius
case "square": return s.side ** 2;
case "rect": return s.w * s.h;
}
}
switch/if trên field này → TS tự narrow type, autocomplete chính xác.4. Intersection — &
// Trộn nhiều type
type WithId = { id: number };
type WithName = { name: string };
type User = WithId & WithName; // { id: number; name: string }
// Generic intersection
type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date };
type UserRecord = WithTimestamps<User>;
| ≠ Intersection &:A | B— value là A HOẶC B (set OR).A & B— value có đủ field A VÀ B (set AND).
5. type vs interface — chọn gì?
// type alias — flexible
type User = { id: number; name: string };
type Id = string | number; // union
type Pair<T> = [T, T]; // tuple
// interface — extensible
interface User {
id: number;
name: string;
}
interface User { age: number; } // declaration merge — User giờ có 3 field
interface Admin extends User {
role: string;
}
interface | type | |
|---|---|---|
| Object shape | ✅ | ✅ |
| Union, intersection, primitive | ❌ | ✅ |
| Tuple, mapped type | ❌ | ✅ |
| Declaration merging | ✅ | ❌ |
| Performance (cho object) | Hơi nhanh hơn | – |
| Khuyến nghị | Object public API | Union, alias, utility |
type vs interface?""Cả 2 mô tả shape object.
interfacechuyên cho object + class hợp đồng, hỗ trợ extends + declaration merging.typeflexible hơn — làm được union, intersection, tuple, mapped type.Rule of thumb: object public API dùng
interface, alias / union / utility dùngtype. Hoặc đơn giản — dùngtypecho mọi thứ, không sai."
6. any vs unknown vs never
never.let x: any = 1;
x.toUpperCase(); // OK compile, BANG runtime
let y: unknown = 1;
y.toUpperCase(); // ❌ error — phải narrow
if (typeof y === "string") {
y.toUpperCase(); // OK — narrowed thành string
}
function fail(msg: string): never {
throw new Error(msg);
}
function exhaustive(s: never): never {
throw new Error(`Unhandled: ${s}`);
}
never cho exhaustive check — pattern cực hay trong PV:type Shape = { kind: "circle" } | { kind: "square" };
function area(s: Shape) {
switch (s.kind) {
case "circle": return ...;
case "square": return ...;
default:
const _exhaustive: never = s; // ⭐ Nếu Shape thêm "rect" mà quên handle
throw new Error(_exhaustive); // → compile error tại đây
}
}
7. Type narrowing — sức mạnh TS
function format(x: string | number) {
// typeof narrowing
if (typeof x === "string") {
return x.toUpperCase(); // narrowed → string
}
return x.toFixed(2); // narrowed → number
}
// instanceof
class Dog {}
class Cat {}
function speak(a: Dog | Cat) {
if (a instanceof Dog) { /* a: Dog */ }
}
// in operator
type Bird = { fly(): void };
type Fish = { swim(): void };
function move(a: Bird | Fish) {
if ("fly" in a) a.fly();
else a.swim();
}
// Truthiness narrowing
function f(s: string | null | undefined) {
if (s) { // s narrowed thành string non-empty
s.length;
}
}
// Equality narrowing
function g(x: string | number, y: string | boolean) {
if (x === y) { // 2 type chung chỉ có string → cả 2 narrowed string
x.toUpperCase();
y.toUpperCase();
}
}
Custom type guard
// Function trả `param is Type` → TS tin để narrow
function isString(x: unknown): x is string {
return typeof x === "string";
}
const arr: unknown[] = ["a", 1, "b"];
const strings = arr.filter(isString); // strings: string[] (auto narrow)
x is Type syntax = type predicate. Cực hay trong PV — phân biệt được fresher / junior:function isUser(obj: unknown): obj is User {
return typeof obj === "object" && obj !== null && "id" in obj && "name" in obj;
}
Template literal type & Unicode (cải thiện TS 7.0)
TS 7.0 xử lý đúng Unicode surrogate pairs khi infer template literal type:
type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never;
type Result = HeadTail<"😀abc">;
// TS 7.0: ["😀", "abc"] ✅ — tách đúng emoji (surrogate pair)
// TS cũ: ["\ud83d", "\ude00abc"] ❌ — tách vỡ surrogate pair
8. Assertion vs Narrowing
// Assertion — TELL compiler, không CHECK
const el = document.getElementById("app") as HTMLDivElement;
const x = unknownThing as User; // unsafe — bạn chịu trách nhiệm
// Non-null assertion (!)
const el2 = document.getElementById("app")!; // = "I'm sure it's not null"
// satisfies (TS 4.9+) — check mà giữ literal narrow
const palette = {
red: [255, 0, 0],
green: "#0f0"
} satisfies Record<string, [number, number, number] | string>;
// palette.red vẫn là [number, number, number], không bị widen
as và ! là escape hatch — bypass type system. Dùng đúng lúc, đừng lạm dụng. Khi bạn as sai → runtime bug.9. Function types
// Function declaration
function add(a: number, b: number): number {
return a + b;
}
// Function type
type Adder = (a: number, b: number) => number;
const sub: Adder = (a, b) => a - b;
// Optional + default + rest
function greet(name: string, greeting = "Hi", ...others: string[]): string {
return `${greeting}, ${name} & ${others.join(",")}`;
}
// Overload
function len(x: string): number;
function len(x: any[]): number;
function len(x: string | any[]): number {
return x.length;
}
// Constructor type
type Ctor<T> = new (...args: any[]) => T;
function create<T>(C: Ctor<T>): T { return new C(); }
Giảm context sensitivity cho function type inference (TS 6.0)
Từ TS 6.0, function không có tham số this sẽ có reduced context sensitivity — type inference đơn giản và dễ đoán hơn:
// Trước TS 6.0: context-sensitive inference có thể gây bất ngờ
// TS 6.0+: inference đơn giản hơn, dễ debug hơn
const fn = (x) => x; // inference rõ ràng, không phụ thuộc ngữ cảnh gọi
10. Enum vs const + as const
// Classic enum — emit code, có thể trick (Reverse mapping)
enum Status { Active, Inactive, Pending } // = 0, 1, 2
let s: Status = Status.Active;
// String enum
enum Color { Red = "RED", Green = "GREEN" }
// const enum — inline, không emit (chỉ literal khi build)
const enum Direction { Up, Down, Left, Right }
preserveConstEnums, và const + as const càng được khuyến khích mạnh mẽ hơn để thay thế hoàn toàn enum:const Status = {
Active: "active",
Inactive: "inactive",
Pending: "pending"
} as const;
type Status = typeof Status[keyof typeof Status];
// ^ "active" | "inactive" | "pending"
11. Object methods & this
type Person = {
name: string;
greet(this: Person): string; // ⭐ this typed
};
const p: Person = {
name: "Alice",
greet() { return `Hi, ${this.name}`; }
};
// Class field arrow vs method — `this` khác
class Counter {
count = 0;
inc() { this.count++; } // bind dynamic — this có thể lost
incArrow = () => { this.count++; }; // bind lexical — this luôn = instance
}
this ra sao?" → dùng arrow function field hoặc .bind(this) trong constructor.12. Index signature
// Bất kỳ key string → value type X
type Dict<T> = { [key: string]: T };
const cache: Dict<number> = { a: 1, b: 2 };
// Mix với known key
type Config = {
apiUrl: string;
timeout: number;
[extra: string]: string | number;
};
// Restricted key — Record (utility type)
type Roles = Record<"admin" | "user" | "guest", string>;
// = { admin: string; user: string; guest: string }
13. Subpath imports #/ (TS 6.0)
TS 6.0 hỗ trợ subpath imports — alias import chuẩn của Node.js, không cần config paths trong tsconfig.json:
// package.json
{
"imports": {
"#utils/*": "./src/utils/*",
"#components/*": "./src/components/*"
}
}
// Dùng ở bất kỳ file nào trong project — không cần relative path dài
import { helper } from "#utils/helper";
import { Button } from "#components/Button";
// TypeScript tự động resolve và check type
#/ imports vs paths trong tsconfig:#/là chuẩn Node.js — hoạt động cả ở runtime lẫn type-check- Không cần config trong
tsconfig.json - Bắt đầu bằng
#để phân biệt với package imports thông thường - Thay thế dần cho
baseUrl+paths(vốn đã bị deprecate từ TS 6.0)