string 字符串类型
let name: string = "Lei";
let greet: string = `Hello, ${name}`; // 模板字符串
let multi: string = `line 1
line 2`; // 多行string 原始类型接受单引号、双引号和反引号模板字面量。要插值或多行就用反引号模板字符串。
TypeScript 速查表,100+ 段代码涵盖类型/泛型/工具类型/类型收窄/异步模式。
let name: string = "Lei";
let greet: string = `Hello, ${name}`; // 模板字符串
let multi: string = `line 1
line 2`; // 多行string 原始类型接受单引号、双引号和反引号模板字面量。要插值或多行就用反引号模板字符串。
let count: number = 42; let pi: number = 3.14; let hex: number = 0xff; // 255 let bin: number = 0b1010; // 10 let big: bigint = 9007199254740993n; // 超过 Number.MAX_SAFE_INTEGER 用 bigint
TypeScript 只有一个 number(IEEE 754)涵盖整数和浮点。超过 2^53 − 1 用 bigint,字面量加 n 后缀。
let done: boolean = false; let ok: boolean = !!getValue(); // 双感叹号转 boolean
只有 true 和 false。把任何值转 boolean 用双感叹号 !!value 这个老套路。
let a: null = null;
let b: undefined = undefined;
let c: string | null = getValue(); // 可能为 null
let d: string | undefined; // 默认 undefined
// strictNullChecks 开了之后必须显式处理
if (c !== null) {
c.toUpperCase(); // ✅ 安全
}null 是显式空值,undefined 是"还没赋值"。开了 strictNullChecks 后(务必开),两者都不能赋给其他类型,必须显式联合。
let x: any = "hello";
x = 42; // 允许
x = { foo: 1 };// 允许
x.bar.baz(); // 不报错,运行时炸
// ❌ 几乎从不该用,优先 unknownany 把类型检查全关了。几乎从不是对的选择,用 unknown 替代。只在从 JS 迁移的边界可以用。
let x: unknown = JSON.parse(input);
// ❌ 直接用会报错
// x.toUpperCase();
// ✅ 必须先收窄
if (typeof x === "string") {
x.toUpperCase(); // 这里 x 已经是 string
}unknown 接受任意值但拿到之后什么也干不了,必须先收窄。解析后的 JSON / 反序列化数据 / 第三方回调都该用它。
function fail(msg: string): never {
throw new Error(msg); // 永远不返回
}
function loop(): never {
while (true) {} // 死循环也是 never
}
// 用在穷举检查
const _exhaustive: never = value;never 表示永远不出现的值,抛异常的函数、死循环、联合里没剩下的分支。用 let _: never = value 在可辨识联合上做穷举检查。
function log(msg: string): void {
console.log(msg);
// 隐式 return undefined
}
// 注意:void 返回类型的函数 PARAM 可以返回任意值
const cb: () => void = () => 42; // ✅ 合法,值会被丢弃void 表示"调用方不该用返回值"。坑:参数位置上的 () => void 接受返回任意值的函数,返回值会被丢弃。
let dir: "up" | "down" | "left" | "right"; dir = "up"; // ✅ // dir = "north"; // ❌ 不在联合里 let status: 200 | 404 | 500; let flag: true; // 唯一一个 true
字符串/数字/布尔字面量都能当类型,变量必须等于联合里某个字面量。可辨识联合和查表常量的基石。
let xs: number[] = [1, 2, 3]; let ys: Array<number> = [1, 2, 3]; // 同义 // 嵌套时 T[] 更清爽 let grid: number[][] = [[1, 2], [3, 4]]; let grid2: Array<Array<number>> = [[1, 2], [3, 4]];
T[] 和 Array<T> 完全等价。简单类型用 T[],复杂联合用 Array<T> 更清楚(Array<string | number>)。
let xs: readonly number[] = [1, 2, 3]; // xs.push(4); ❌ Property 'push' does not exist // xs[0] = 9; ❌ Index signature is readonly // 等价语法 let ys: ReadonlyArray<number> = [1, 2, 3];
readonly T[] 去掉所有 mutating 方法(push、pop、splice)也不能下标赋值。函数参数标 readonly 表示"我不会改它"。
let pair: [string, number] = ["age", 30]; let trio: [string, number, boolean] = ["x", 1, true]; // 命名元组(4.0+),仅文档用途 let coord: [x: number, y: number] = [10, 20]; // 可选元组元素 let opt: [string, number?] = ["x"]; // rest 元组元素 let rest: [string, ...number[]] = ["x", 1, 2, 3];
元组是定长数组,每个位置类型独立。命名标签(4.0+)只是文档用途。[string, number?] 允许末尾可选;[T, ...U[]] 允许尾部 rest。
let o: object = { x: 1 };
o = [1, 2, 3]; // ✅ 数组也是 object
o = () => {}; // ✅ 函数也是 object
// o = "hello"; // ❌ 字符串是 primitive
// ⚠️ object 上几乎什么也拿不到,你要的多半是 Record<string, unknown>
let m: Record<string, unknown> = { a: 1, b: "x" };object 表示"不是原始类型的任何东西",数组、函数、普通对象都算。上面拿不到任何属性;想要类型化字典用 Record<string, unknown>。
enum Color { Red, Green, Blue } // 0, 1, 2
let c: Color = Color.Red; // 0
Color[0]; // "Red" (反查)
enum Status { Ok = "OK", Err = "ERR" } // 字符串 enumenum 生成运行时对象,数字 enum 双向映射(数字 ↔ 名字),字符串 enum 单向。新代码通常优先字符串字面量联合。
const id: symbol = Symbol("id");
const a = Symbol("x");
const b = Symbol("x");
a === b; // false,每个 Symbol 都唯一
// unique symbol 才能当类型层面的常量键
const KEY: unique symbol = Symbol();
interface Box { [KEY]: number }每次 Symbol() 调用都返回唯一值,所以相同描述的两个 symbol 也不相等。只有 const x: unique symbol 才能在类型里当计算属性键。
const enum Dir { Up, Down }
const d = Dir.Up;
// 编译输出直接内联为: const d = 0;
// 不生成 Dir 这个运行时对象
// ⚠️ 与 isolatedModules / 单文件转译(Babel、esbuild)冲突const enum 在编译期被擦除,成员直接内联成字面量,不生成运行时对象。它与 isolatedModules 以及 Babel、esbuild 这类单文件转译器冲突。
let add: (a: number, b: number) => number; add = (x, y) => x + y; // 参数类型自动推断 // 类型别名形式 type BinOp = (a: number, b: number) => number; const mul: BinOp = (a, b) => a * b;
函数类型 (a: T, b: U) => R 描述参数和返回值。把符合的箭头函数赋给它时,参数类型按上下文自动推断,不用再标一遍。
function greet(name: string, greeting?: string) {
return `${greeting ?? "Hi"}, ${name}`; // greeting: string | undefined
}
function greet2(name: string, greeting = "Hi") {
return `${greeting}, ${name}`; // greeting: string,永远有值
}p?: T 让参数变成 T | undefined,你得处理缺失的情况。默认值 p = value 则从类型里去掉 undefined,省略实参时自动填上。
function len(x: string): number;
function len(x: any[]): number;
function len(x: string | any[]): number {
return x.length;
}
len("abc"); // ✅ number
len([1, 2, 3]); // ✅ number重载签名在一个实现之上声明多种调用形状。调用方只能看到重载签名,实现签名对外隐藏,且必须兼容所有重载。
interface Counter { count: number }
function inc(this: Counter, by: number) {
this.count += by; // this 已知是 Counter
}
const c: Counter = { count: 0 };
inc.call(c, 5); // ✅ this 类型被检查名为 this 的伪首参用来标注接收者,它不出现在实际参数列表里。之后编译器会检查 call / apply / bind 传入的 this 是否兼容。
interface User {
id: number;
name: string;
email?: string; // 可选属性
readonly createdAt: Date; // 只读
}
const u: User = { id: 1, name: "Lei", createdAt: new Date() };对象形状用 interface。? 标可选,readonly 禁止赋值。日常建模实体和 props 的首选。
type ID = string | number;
type Point = { x: number; y: number };
type Callback = (err: Error | null, data?: string) => void;
type Tuple = [string, number];type 别名给任意类型起名,联合、元组、函数、原始类型。interface 做不了联合,type 可以。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 多继承
interface Cat extends Animal, Pet {
livesLeft: number;
}interface 可继承一个或多个 interface。子接口拿到所有父接口属性加自己的。属性冲突直接在声明处报错。
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // { name: string; age: number }
const p: Person = { name: "Lei", age: 30 };type 别名用交叉 & 合并形状,等价于 interface extends,但适用任意类型,不限于 interface。
interface StringMap {
[key: string]: string;
}
const m: StringMap = {};
m["lang"] = "zh";
m["theme"] = "dark";
// 同时有已知键 + 动态键
interface Config {
apiUrl: string;
[key: string]: string;
}索引签名让你给类似字典的对象写动态 key 类型。所有已知键的值类型必须与索引签名一致。
interface Greeter {
(name: string): string; // 可调用
greeting: string; // 还有属性
}
const g: Greeter = ((name: string) => `${g.greeting}, ${name}`) as Greeter;
g.greeting = "Hello";
g("Lei"); // "Hello, Lei"interface 可声明调用签名,描述一个既是函数又有属性的对象。给"函数对象"(带 displayName 的 React FC 等)建模很好用。
// ✅ 对象形状要被 extend / implement → interface
interface User { id: number; name: string }
// ✅ 联合 / 元组 / 映射 / 条件 → type
type ID = string | number;
type Tuple = [string, number];
type Keys<T> = keyof T;经验法则:要被 extend / implement 的对象形状用 interface;联合 / 元组 / 计算出来的形状用 type。库的公共 API 倾向 interface,内部计算类型倾向 type。
interface UserCtor {
new (id: number, name: string): { id: number; name: string };
}
function create(Ctor: UserCtor, id: number, name: string) {
return new Ctor(id, name);
}new (...): T 签名描述构造函数类型,让值可以配 new 使用。工厂函数接收一个类引用再去实例化时常用。
interface Counter {
(start: number): string; // 可调用
interval: number; // 属性
reset(): void; // 方法
}
function make(): Counter {
const c = ((start: number) => String(start)) as Counter;
c.interval = 123;
c.reset = () => {};
return c;
}一个 interface 可同时含调用签名、属性和方法,建模函数对象。jQuery 那种既能调用又挂了一堆命名空间方法的 API 是经典例子。
interface Serializable {
serialize(): string;
}
class User implements Serializable {
constructor(public id: number) {}
serialize() { return JSON.stringify({ id: this.id }); }
}class C implements I 让编译器校验该类拥有 I 的每个成员。这只是结构检查,不像 extends 那样继承实现。
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
const data: Json = { a: 1, b: [true, null, { c: "x" }] };type 别名可以引用自身,用来建模 JSON、树、嵌套菜单这类递归结构。联合里每个分支都能各自递归。
interface Plugin {
name: string;
setup?(): void; // 可选方法
teardown?: () => void; // 可选属性形式的方法
}
const p: Plugin = { name: "x" };
p.setup?.(); // 可选链调用
p.teardown?.();m?(): void 和 m?: () => void 都让方法可选,调用时走 obj.m?.()。属性形式在 strictFunctionTypes 下逆变,简写形式则双变。
type StringOrNumber = string | number;
function format(x: string | number): string {
if (typeof x === "string") return x.toUpperCase();
return x.toFixed(2);
}联合 A | B 接受任一种。除非先收窄,否则只能调用两边都有的方法/属性。
type WithId = { id: string };
type WithName = { name: string };
type Entity = WithId & WithName; // 同时有 id 和 name
const e: Entity = { id: "a1", name: "Lei" };交叉 A & B 要求同时拥有 A 和 B 的所有属性。组合能力(capability)时常用。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; w: number; h: number }
| { kind: "triangle"; base: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.w * s.h;
case "triangle": return 0.5 * s.base * s.h;
}
}共同的判别字段(这里是 kind)让联合可通过 switch 收窄。建模变体数据最可靠的套路。
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.w * s.h;
default:
const _exhaustive: never = s; // ✅ 漏掉一个分支这里直接报错
throw new Error(`Unknown shape: ${_exhaustive}`);
}
}把没处理的值赋给 never 类型,少一个分支就编译报错,专治"加了新变体忘改 switch"的 bug。
type Status = "idle" | "loading" | "success" | "error"; let s: Status = "idle"; s = "loading"; // ✅ // s = "fetching"; // ❌ not in union
字符串字面量联合是 enum 的现代替代,零运行时成本、自动补全完美、能直接 JSON 序列化。
type Maybe = { profile?: { name?: string } } | null;
function getName(u: Maybe): string | undefined {
return u?.profile?.name; // 任一环为空就短路返回 undefined
}可选链 ?. 在左侧为 null 或 undefined 时立即短路返回 undefined。结果类型一定带上 undefined,避免你忘了处理空值。
function port(input?: number) {
const a = input ?? 8080; // 只在 null/undefined 时用默认
const b = input || 8080; // 0 也会被替换掉!
return [a, b];
}
port(0); // [0, 8080]?? 只在左侧是 null 或 undefined 时才用默认值,而 || 连 0、""、false 也一并替换。数值和布尔默认值用 ??,免得把合法的假值也吞掉。
type Handler = ((e: string) => void) | ((e: number) => void);
// ⚠️ 调用联合函数时,参数类型取交集(never)
declare const h: Handler;
// h("x"); ❌ 参数被推为 string & number = never
// 多数时候你想要的是单个带联合参数的函数:
type Better = (e: string | number) => void;调用函数类型的联合时,实参要同时满足所有成员,于是参数被压成交集(常是 never)。多数情况下你真正想要的是一个参数为联合类型的单函数。
type Result<T> =
| { status: "ok"; data: T }
| { status: "err"; message: string };
function unwrap<T>(r: Result<T>): T {
if (r.status === "ok") return r.data; // r.data 可用
throw new Error(r.message); // r.message 可用
}检查判别字段(这里是 status)会收窄联合,于是分支专属的 payload 变得可访问。这是类型化 Result 和远程数据状态机的主干。
function identity<T>(x: T): T {
return x;
}
const s = identity("hello"); // T 推断为 string
const n = identity(42); // T 推断为 number
const arr = identity<number[]>([1, 2, 3]); // 显式传 T类型参数 <T> 让函数适用任意类型,同时保留输入输出的类型关系。T 一般推断,推断不出来才显式传。
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("lei", 30); // [string, number]
function map<T, U>(xs: T[], fn: (x: T) => U): U[] {
return xs.map(fn);
}多个类型参数维持多边关系。map<T, U> 把输入数组元素类型和回调参数挂钩,回调返回类型决定输出数组元素类型。
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("abc", "ab"); // ✅ string 有 length
longest([1, 2, 3], [1]); // ✅ array 有 length
// longest(42, 99); // ❌ number 没有 lengthT extends Shape 限制 T 必须符合 Shape,函数体内可以用约束里的属性。比"需要某个属性"用 any 强多了。
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
const a: ApiResponse = { data: "?", status: 200 }; // T = unknown
const b: ApiResponse<User> = { data: { id: 1 } as User, status: 200 };默认 <T = X> 让调用方可省略类型参数走默认值。框架代码常见(React.Component<P = {}, S = {}>)。
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Lei" };
pluck(user, "id"); // number
pluck(user, "name"); // string
// pluck(user, "age"); // ❌ "age" 不是 user 的 keyK extends keyof T 约束 K 必须是 T 的某个属性名,返回 T[K] 给出精确的属性类型。lodash 的 _.pick 类型安全就靠它。
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const r: Result<number> = { ok: true, value: 42 };type 别名也可以带类型参数。Result wrapper、容器类型、计算出来的形状常用。
interface Cache<T> {
get(key: string): T | undefined;
set(key: string, value: T): void;
}
const userCache: Cache<User> = makeCache<User>();interface 也可以带类型参数。实例化时定型,Cache<User> 的 get 返回 User | undefined。
interface ListProps<T> {
items: T[];
render: (item: T) => React.ReactNode;
}
function List<T>({ items, render }: ListProps<T>) {
return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}
<List items={users} render={(u) => u.name} /> // T 自动推断 User泛型组件把 item 类型传到所有 props,render 拿到的是真正的 T,不是 any。带类型的 List / Table / Select 组件标准套路。
class Stack<T> {
private items: T[] = [];
push(x: T): void { this.items.push(x); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
}
const s = new Stack<number>();
s.push(1);
s.push(2);
s.pop(); // number | undefinedclass 也能带类型参数。new Stack<number>() 时定型,所有方法都用这个 T。
function asTuple<const T extends readonly unknown[]>(t: T): T {
return t;
}
const a = asTuple([1, 2, 3]); // readonly [1, 2, 3],不是 number[]
// const T 让推断像加了 as const 一样窄const 类型参数(<const T>)从实参推断出最窄的字面量类型,效果如同调用方写了 as const。它避免把元组放宽成数组、字面量放宽成基础类型。
function merge<T extends object, U extends object>(a: T, u: U): T & U {
return { ...a, ...u };
}
const r = merge({ id: 1 }, { name: "Lei" });
// r: { id: number } & { name: string }每个类型参数都能带自己的 extends 约束,返回类型可用 & 把它们合起来。类型化的 Object.assign 风格合并就靠这个保留两边形状。
interface Reducer<S, A = { type: string }> {
(state: S, action: A): S;
}
// A 默认 { type: string },也可显式传更精确的 action 联合
type CountReducer = Reducer<number>;靠后的类型参数默认值可引用靠前的参数,比如 <S, A = S[]>。框架签名用它让调用方只填自己真正关心的参数。
function first<T extends readonly [unknown, ...unknown[]]>(t: T): T[0] {
return t[0];
}
first([1, "a", true]); // 1,类型 number
// first([]); ❌ 空元组不满足约束约束成 readonly [unknown, ...unknown[]] 要求至少一个元素,于是 t[0] 取值可证安全。空数组在调用点直接被编译器拒绝。
function prop<K extends string>(key: K) {
return <T extends Record<K, unknown>>(obj: T): T[K] => obj[key];
}
const getName = prop("name");
getName({ name: "Lei", age: 30 }); // string从外层函数返回一个泛型箭头,把一个类型参数带进内层作用域。内层函数每次调用仍重新推断 T,保持 helper 可复用。
interface User { id: number; name: string; email: string }
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }
function update(id: number, patch: Partial<User>) { /* ... */ }
update(1, { name: "Lei" }); // ✅ 只传一个属性Partial<T> 把每个属性变成可选。最常见用途:PATCH 风格的更新函数接受部分字段。
interface Config { host?: string; port?: number }
type FullConfig = Required<Config>;
// { host: string; port: number }
function start(c: Required<Config>) { /* ... */ }Required<T> 去掉所有 ? 可选标记。配置带默认值填充后想断言"全设好了"时用。
interface User { id: number; name: string }
type FrozenUser = Readonly<User>;
const u: FrozenUser = { id: 1, name: "Lei" };
// u.name = "Han"; // ❌ readonlyReadonly<T> 给每个属性加 readonly。编译期冻结,零运行时成本。要深度冻结需要递归版本。
interface User { id: number; name: string; email: string; password: string }
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }Pick<T, K> 只挑指定 key 构造新类型。视图模型 / DTO 从更大的实体派生时用。
interface User { id: number; name: string; password: string }
type SafeUser = Omit<User, "password">;
// { id: number; name: string }Omit<T, K> 是 Pick 的反操作,除了列出的 key 全保留。绝大多数字段都要时比 Pick 简洁。
type Role = "admin" | "user" | "guest";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};Record<K, V> 等价于 { [P in K]: V },key 是 K,value 是 V。K 是字面量联合时编译器会校验每个 key 都被填上。
type Status = "idle" | "loading" | "success" | "error"; type Resolved = Exclude<Status, "idle" | "loading">; // "success" | "error"
Exclude<T, U> 从 T 里去掉能赋给 U 的成员,对联合做差集。
type All = string | number | boolean | null; type Numerics = Extract<All, number | bigint>; // number
Extract<T, U> 只保留 T 里能赋给 U 的成员,对联合取交集。
type Maybe = string | number | null | undefined; type Real = NonNullable<Maybe>; // string | number
NonNullable<T> 同时去掉 null 和 undefined。用 x != null 收窄后常用。
function getUser() {
return { id: 1, name: "Lei", email: "x@y.z" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }ReturnType<F> 抽出函数类型的返回值。配 typeof fn 用,从工厂函数推导出实体类型,不用重复写形状。
function login(user: string, pwd: string, remember?: boolean) { /* ... */ }
type LoginArgs = Parameters<typeof login>;
// [user: string, pwd: string, remember?: boolean | undefined]Parameters<F> 返回函数参数类型组成的元组。包装函数需要原样转发参数列表时常用。
type T1 = Awaited<Promise<string>>; // string type T2 = Awaited<Promise<Promise<number>>>; // number (递归解) type T3 = Awaited<number>; // number (不是 Promise 就原样)
Awaited<T> 递归解开嵌套 Promise。4.5+ 后是类型系统里 await 语义的官方实现。
class User {
constructor(public id: number, public name: string) {}
}
type Args = ConstructorParameters<typeof User>;
// [id: number, name: string]ConstructorParameters<C> 抽出构造函数的参数元组。工厂函数想完全镜像 class constructor 时用。
class User { /* ... */ }
type U = InstanceType<typeof User>;
// UserInstanceType<C> 给出 new C() 的类型。主要配 mixin 和 typeof Class 引用实例类型时用。
type H = Uppercase<"hello">; // "HELLO" type W = Lowercase<"WORLD">; // "world" type T = Capitalize<"foo">; // "Foo" type U = Uncapitalize<"Bar">; // "bar"
内置 intrinsic 类型,在类型层做字符串字面量变换。构造带类型的事件名、CSS-in-JS key 时用。
function fn(this: { x: number }, y: number) {
return this.x + y;
}
type Plain = OmitThisParameter<typeof fn>;
// (y: number) => numberOmitThisParameter<F> 从函数类型里去掉 this 参数,留下纯粹的可调用签名。bind 掉接收者之后建模常用。
function fn(this: { x: number }, y: number) {
return this.x + y;
}
type Self = ThisParameterType<typeof fn>;
// { x: number }ThisParameterType<F> 抽出函数声明的 this 参数类型。包装或重绑方法时配 OmitThisParameter 用。
function paint<C extends string>(
palette: C[],
fallback: NoInfer<C>,
) { /* ... */ }
paint(["red", "blue"], "red"); // ✅
// paint(["red", "blue"], "x"); ❌ "x" 不在 palette 推断出的 C 里NoInfer<T> 告诉编译器推断类型参数时别参考这个位置。于是 C 由第一个参数定死,第二个参数只用来对照校验。
type Lang = "en" | "zh" | "ja";
type Translations = Partial<Record<Lang, string>>;
const t: Translations = { en: "Hello" }; // zh / ja 可缺省把 Record<K, V> 套进 Partial,得到每个 key 都可选的字典,可只填已知键集合的一部分。部分 i18n 词包和功能开关常用。
interface User { id?: number; name?: string; email?: string }
type WithId = User & Required<Pick<User, "id">>;
// id 必填,name / email 仍可选
const u: WithId = { id: 1 }; // ✅把类型与 Required<Pick<T, K>> 交叉,只强制选中的键必填,其余仍可选。"这个字段现在保证存在"的视图模型很好用。
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
const frozen = { x: 1, y: 2 } as const; // readonly { x: 1; y: 2 }
type Editable = Mutable<typeof frozen>;
// { x: 1; y: 2 },可改TypeScript 没有内置 Mutable,去除只读的标准写法就是 -readonly 映射修饰符。常配那些先 as const、之后又要改的数据用。
const ROLES = ["admin", "user", "guest"] as const; type Role = typeof ROLES[number]; // "admin" | "user" | "guest"
用 [number] 索引只读元组类型,得到所有元素类型的联合。这是从单一事实数组派生字符串字面量联合的标准技巧。
function format(x: string | number): string {
if (typeof x === "string") {
return x.toUpperCase(); // x: string
}
return x.toFixed(2); // x: number
}typeof 返回 "string" | "number" | "boolean" | "object" | "function" | "undefined" | "symbol" | "bigint"。编译器在分支里自动收窄。
class HttpError extends Error { status: number = 500 }
class NetError extends Error { code: string = "ECONN" }
function handle(e: HttpError | NetError) {
if (e instanceof HttpError) {
console.log(e.status); // e: HttpError
} else {
console.log(e.code); // e: NetError
}
}instanceof Class 收窄到 class 类型。任何构造函数都行,内置的 Error / Date 和用户类都可以。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(a: Fish | Bird) {
if ("swim" in a) {
a.swim(); // a: Fish
} else {
a.fly(); // a: Bird
}
}"key" in obj 收窄到联合里有这个 key 的成员。不想加判别字段时比可辨识联合更轻量。
function example(x: string | null) {
if (x !== null) {
x.toUpperCase(); // x: string
}
}
function check(a: "a" | "b", b: "b" | "c") {
if (a === b) {
// 这里 a 和 b 都是 "b",唯一公共值
}
}=== 和 !== 收窄联合成员。两个联合互相比较时,两边都收窄到公共成员。
function example(x: string | null | undefined) {
if (x) {
x.toUpperCase(); // x: string (null / undefined / "" 都过滤了)
}
}
// 注意:空字符串也会被过滤
function trap(x: string | null) {
if (x) {
// x: string,但 "" 也被排除了!
}
}真值判断 if (x) 过滤 null、undefined、0、""、NaN、false。坑:空字符串也算 false,往往不是你想要的。
function isString(x: unknown): x is string {
return typeof x === "string";
}
function example(x: unknown) {
if (isString(x)) {
x.toUpperCase(); // x: string
}
}返回类型写 x is T 告诉编译器"此函数返回 true 当且仅当 x 是 T"。教类型系统理解自定义检查的途径。
function assertString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new Error("not string");
}
function example(x: unknown) {
assertString(x);
x.toUpperCase(); // x: string (assert 之后整段都是 string)
}asserts x is T 在调用之后收窄(不是在 if 分支里)。想要抛异常而不是返回 boolean 的守卫时用。
type Action =
| { type: "ADD"; payload: number }
| { type: "RESET" }
| { type: "SET"; payload: number };
function reduce(state: number, a: Action): number {
switch (a.type) {
case "ADD": return state + a.payload;
case "RESET": return 0;
case "SET": return a.payload;
default:
const _: never = a; // 漏一个分支编译报错
return state;
}
}Redux 风格的可辨识联合 reducer 完整模式,带穷举检查。新加一种 action 类型不处理就编译报错。
function flatten(x: string | string[]): string {
if (Array.isArray(x)) {
return x.join(", "); // x: string[]
}
return x; // x: string
}Array.isArray(x) 是内置类型守卫,把 T | T[] 收窄到数组分支。接受"一个或多个"参数时最可靠的写法。
type Resp =
| { ok: true; body: string }
| { ok: false; code: number };
function read(r: Resp) {
if (r.ok) return r.body; // ok: true 分支
return `error ${r.code}`; // ok: false 分支
}布尔(或任意字面量)字段和字符串 kind 一样能当判别字段。检查 r.ok 就收窄了联合,不必专门加 tag 字符串。
function handle(method: string) {
const m = method.toUpperCase();
if (m === "GET" || m === "POST") {
const verb = m as "GET" | "POST"; // 明确收窄
return verb;
}
}把放宽过的 string 与字面量比较后,用 as 断言把它锁回字面量联合供后续使用。最好一开始就标窄类型,但这招能救已经放宽的值。
let x: string | number; x = "hello"; x.toUpperCase(); // x: string,赋值后立即收窄 x = 42; x.toFixed(2); // x: number
赋值会把变量在后续语句里收窄到该值的类型,即便声明类型是联合。这是你天天依赖却毫无察觉的控制流分析。
function run(cfg: { value?: number }) {
// cfg.value 每次访问可能被别处改,TS 不替你收窄
const v = cfg.value;
if (v !== undefined) {
v.toFixed(2); // ✅ 局部常量收窄成立
}
}对 obj.prop 的收窄可能被中间的调用打破,所以先拷进一个 const 再收窄它。局部绑定可证不变,守卫才稳。
type Stringify<T> = {
[K in keyof T]: string;
};
interface User { id: number; age: number }
type UserStrings = Stringify<User>;
// { id: string; age: string }{ [K in keyof T]: ... } 遍历 T 的 key 生成新类型。Partial、Required、Readonly 都是基于映射类型实现的。
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User { id: number; name: string }
type N = Nullable<User>;
// { id: number | null; name: string | null }用 T[K] 引用原值类型再变换(这里联合 null)。"所有字段都允许 null"的包装常用。
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { id: number; name: string }
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }as 子句在映射时重命名 key。配合模板字面量类型,可从基类型生成 getter / setter / 事件名的形状。
type PickStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed { name: string; age: number; role: string }
type Strings = PickStrings<Mixed>;
// { name: string; role: string }把 key 映射到 never 就会从结果里删掉。配合条件类型,可按值类型过滤对象属性。
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]+?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
interface Frozen { readonly id: number; readonly name: string }
type Editable = Mutable<Frozen>;
// { id: number; name: string }- 去掉 optional 或 readonly,+ 加上(默认)。内置 Required 和 Mutable 工具就靠这个机制实现。
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Tree { left: Tree | null; value: number }
type FrozenTree = DeepReadonly<Tree>;
// 整棵树都是 readonly递归映射类型把嵌套对象属性也冻起来。排除 Function(typeof function === "object")避免方法被改坏。
type FormValues<Fields extends string> = {
[K in Fields]: string;
};
type LoginForm = FormValues<"email" | "password">;
// { email: string; password: string }
const f: LoginForm = { email: "x@y.z", password: "secret" };
// 漏 email 编译报错遍历有限字符串联合要求每个 key 都在场。编译器校验完整性,表单 schema、权限表、路由表常用。
type Clone<T> = { [K in keyof T]: T[K] };
interface User { readonly id: number; name?: string }
type Copy = Clone<User>;
// { readonly id: number; name?: string },readonly / ? 都保留写成 [K in keyof T] 的映射类型是同态的,会自动复制每个属性的 readonly 和 ? 修饰符。这正是 Partial / Readonly 能保留原结构的原因。
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
type Flags = MyRecord<"a" | "b", boolean>;
// { a: boolean; b: boolean }在 key 的联合上映射 [P in K] 正是内置 Record<K, V> 的定义方式。keyof any(string | number | symbol)是最宽的合法 key 约束。
type Boxed<T> = {
[K in keyof T]: T[K] extends string ? { text: T[K] } : { value: T[K] };
};
interface M { name: string; age: number }
type R = Boxed<M>;
// { name: { text: string }; age: { value: number } }在映射类型里可对 T[K] 用条件类型分支,按每个值的类型分别变换。常用于按原类型把字段包进带标签的容器。
type Boxed<T> = { [K in keyof T]: { v: T[K] } };
type R = Boxed<[number, string]>;
// [{ v: number }, { v: string }],元组结构保留在元组上映射会保持同长度的元组,逐位变换每个元素。能感知数组/元组的映射类型撑起了逐元素包装这类工具。
type IsString<T> = T extends string ? "yes" : "no"; type A = IsString<"hello">; // "yes" type B = IsString<42>; // "no" type C = IsString<string>; // "yes"
T extends U ? X : Y 根据可赋值性选类型。所有"聪明"工具类型的底层机制。
type ToArray<T> = T extends any ? T[] : never; type A = ToArray<string | number>; // string[] | number[] (而不是 (string | number)[]) // // 联合传入裸类型参数时,条件类型会分配到每个成员
类型参数是"裸"的(就是 T,没包起来)时,条件类型对联合的每个成员分配。要阻止分配就包起来:[T] extends [U]。
type IsUnion<T, _T = T> = T extends any
? [Exclude<_T, T>] extends [never]
? false
: true
: never;
type A = IsUnion<string | number>; // true
type B = IsUnion<string>; // false把类型参数包到元组里 [T] 阻止分配。用于判断"这是不是一个联合"的套路。
type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type A = TypeName<"hello">; // "string" type B = TypeName<() => void>;// "function"
条件类型链模拟 switch / 模式匹配。从上往下,先匹配的赢。
type NonEmpty<T extends string> = T extends "" ? never : T;
type A = NonEmpty<"hello">; // "hello"
type B = NonEmpty<"">; // never
function tag<T extends string>(s: NonEmpty<T>): T {
return s;
}
// tag(""); ❌ "" 推断为 never,无法调用泛型约束配条件类型在调用点验证输入形状。NonEmpty<""> 解析为 never,调用直接报错。
// 内置实现: type NonNullable<T> = T extends null | undefined ? never : T; type A = NonNullable<string | null>; // string type B = NonNullable<string | undefined>; // string type C = NonNullable<null>; // never
NonNullable 用分配律条件类型把 null/undefined 映射到 never。读内置工具类型实现是吃透条件类型的最快路径。
type Flatten<T> = T extends (infer E)[] ? E : T; type A = Flatten<string[]>; // string type B = Flatten<number>; // number(不是数组就原样)
带 infer E 的条件类型在 T 是数组时剥掉一层,否则原样返回 T。标准库的 FlatArray 就是这个形状。
type DeepFlatten<T> = T extends (infer E)[] ? DeepFlatten<E> : T; type A = DeepFlatten<number[][][]>; // number type B = DeepFlatten<string>; // string
在 infer E 分支里递归,可把任意深度的嵌套数组压到叶子类型。递归条件类型是类型层面的 while 循环。
type Unbox<T, F = T extends Promise<infer U> ? U : T> = F; type A = Unbox<Promise<number>>; // number type B = Unbox<string>; // string
默认类型参数本身可以是一个从前置参数计算出来的条件类型。它让你对外暴露派生类型,同时保持公共签名简短。
type Unwrap<T> = T extends () => infer R ? R : T; type A = Unwrap<() => number>; // number type B = Unwrap<string>; // string
推断无参函数的返回类型、否则回退到值本身,建模了"惰性或即时"输入。常用于既接受值又接受工厂函数的配置项。
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never; type R1 = MyReturnType<() => string>; // string type R2 = MyReturnType<(x: number) => boolean>; // boolean
infer R 给你想抽出的类型位置起名字。内置的 ReturnType 就是这个套路。
type MyAwaited<T> = T extends Promise<infer U> ? U : T; type A = MyAwaited<Promise<string>>; // string type B = MyAwaited<number>; // number
在 Promise<...> 里用 infer 抽出 resolved 类型。内置 Awaited 还会递归处理嵌套 Promise。
type Element<T> = T extends (infer E)[] ? E : never;
type A = Element<number[]>; // number
type B = Element<Array<{ x: number }>>;// { x: number }(infer E)[] 模式抽出数组元素类型。等价于数组上的 T[number] 查表写法。
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never; type Tail<T extends any[]> = T extends [any, ...infer R] ? R : []; type H = Head<[string, number, boolean]>; // string type T = Tail<[string, number, boolean]>; // [number, boolean]
变长元组类型配 infer 可以拆元组,头、尾、最后一个、init。编译期 list 操作的基础。
type FirstParam<F> = F extends (first: infer P, ...rest: any[]) => any ? P : never; type A = FirstParam<(name: string, age: number) => void>; // string
在参数列表第一个位置用 infer,剩下忽略。镜像内置 Parameters,但只要第一个。
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never; type A = Last<[1, 2, 3]>; // 3 type B = Last<[]>; // never
剩余模式 [...any[], infer L] 抓住元组最后一个元素。变长元组位置配 infer,可以不递归地读出头、尾、末元素。
type Resolved<T> = T extends Promise<infer U> ? Resolved<U> : T; type A = Resolved<Promise<Promise<number>>>; // number type B = Resolved<string>; // string
对推断出的 U 递归,可把任意层嵌套的 Promise 解到最终值,镜像内置 Awaited。非 Promise 分支终止递归。
type FirstString<T> = T extends [infer S extends string, ...any[]] ? S : never; type A = FirstString<["hi", 1, 2]>; // "hi" type B = FirstString<[1, 2]>; // never
4.7 起可用 infer S extends C 给推断结果加约束,只有推断类型符合时分支才命中。省去再嵌一层条件类型去校验推断结果。
type PropType<T, K extends string> =
T extends { [P in K]: infer V } ? V : never;
type A = PropType<{ id: number; name: string }, "id">; // number把 infer V 放在属性位置,从匹配的对象形状里抽出该字段类型。它把索引访问 T[K] 推广成编译器可结构匹配的模式。
type RestArgs<F> = F extends (first: any, ...rest: infer R) => any ? R : never; type A = RestArgs<(id: number, a: string, b: boolean) => void>; // [a: string, b: boolean]
推断 ...rest: infer R 把第一个之后的所有参数抓成元组。包装函数固定首参、转发其余参数时常用。
type Greeting = `hello ${string}`;
const g1: Greeting = "hello world"; // ✅
const g2: Greeting = "hello there"; // ✅
// const g3: Greeting = "hi"; // ❌ 不以 "hello " 开头模板字面量类型(4.1+)按模式约束字符串。嵌 string 让那段可变;嵌字面量联合就是有限集合。
type Lang = "en" | "zh";
type Kind = "button" | "link";
type Key = `${Kind}.${Lang}`;
// "button.en" | "button.zh" | "link.en" | "link.zh"模板里嵌联合得到笛卡尔积。i18n key、事件名、CSS 类名常用。
type EventNames<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]?: (value: T[K]) => void;
};
interface Form { name: string; age: number }
type FormHandlers = EventNames<Form>;
// { onName?: (v: string) => void; onAge?: (v: number) => void }映射类型 + key 重命名 + 模板字面量组合,自动生成 handler 类型。类型安全事件 API 的底层套路。
type Split<S extends string, D extends string> =
S extends `${infer A}${D}${infer B}`
? [A, ...Split<B, D>]
: [S];
type A = Split<"a,b,c,d", ",">; // ["a", "b", "c", "d"]递归模板字面量配 infer 可在编译期切字符串、解析路由、校验字符串形状。
type Id = `user_${number}`;
const a: Id = "user_42"; // ✅
// const b: Id = "user_x"; ❌ 必须是数字
type Hex = `#${string}`;
const c: Hex = "#ff0000"; // ✅模板类型里嵌 number 或 string 约束整体形状,同时让那一段自由。适合带前缀的 id 格式、十六进制颜色、加前缀的 key。
type Method = "get" | "post";
type Route = `${Uppercase<Method>} /api`;
// "GET /api" | "POST /api"Uppercase 这类 intrinsic 字符串类型能直接嵌进模板字面量,在生成笛卡尔积时变换每个联合成员。HTTP 方法表、事件通道常用。
type RemovePrefix<S extends string, P extends string> =
S extends `${P}${infer Rest}` ? Rest : S;
type A = RemovePrefix<"on:click", "on:">; // "click"
type B = RemovePrefix<"click", "on:">; // "click"匹配 `${P}${infer Rest}` 去掉已知前缀并抓住剩余部分。可在类型层解析事件名、带命名空间的 key、CSS 变量前缀。
type Join<T extends string[], D extends string> =
T extends [infer F extends string, ...infer R extends string[]]
? R extends []
? F
: `${F}${D}${Join<R, D>}`
: "";
type A = Join<["a", "b", "c"], "/">; // "a/b/c"递归模板字面量用分隔符把字符串元组连起来,是 Split 的类型层逆操作。空剩余分支避免末尾多出分隔符。
const el = document.getElementById("input") as HTMLInputElement;
el.value = "hello"; // ✅ 编译器信你说的是 input
// ❌ 不检查,错了运行时炸
const wrong = "hello" as unknown as number;as Foo 是不检查的强转,编译器信你。只有当你真的知道得比编译器多时才用。绝不能当"关掉类型错误"的锤子。
const a = "hello"; // type: string
const b = "hello" as const; // type: "hello"
const arr1 = [1, 2, 3]; // type: number[]
const arr2 = [1, 2, 3] as const;// type: readonly [1, 2, 3]
const obj = { x: 1, y: 2 } as const;
// type: { readonly x: 1; readonly y: 2 }as const 把值冻到最窄类型,字符串保留字面量、数组变只读元组、对象深只读。查表常量和配置常用。
type Config = Record<string, string | number>;
const config = {
port: 8080,
host: "localhost",
protocol: "https",
} satisfies Config;
config.port; // number (推断保留)
config.protocol; // "https" (字面量保留)
// ❌ 用 : Config 标注会放宽到 string | number 丢失字面量satisfies 校验值符合类型但不放宽推断类型。配置对象想同时拿到编译期校验和窄字面量类型查表时的最佳选择。
const el = document.getElementById("input")!; // 断言不是 null
el.click();
// ❌ 滥用会埋雷
// 更安全:
const el2 = document.getElementById("input");
if (el2) el2.click();后缀 ! 告诉编译器"我保证这不是 null/undefined"。DOM 查询是主要合法用例,即便如此,更推荐显式 null 检查。
const a = <string>someValue; // 旧写法 const b = someValue as string; // ✅ 推荐(JSX 不冲突)
老的 <Type>value 写法在 JSX 里冲突。TSX 文件必须用 as Type(其他地方也建议统一用 as 保持一致)。
// ❌ 直接断不让你过 // const n = "hello" as number; // ✅ 经过 unknown 二段断言(编译器允许,但你自己要负责) const n = "hello" as unknown as number; // 真要这么干说明设计有问题,重新想想类型
源类型和目标类型重叠不够时 TS 拒绝直接断。as unknown as Foo 是逃生口,几乎一定意味着类型设计有问题,需要重新想。
type Route = { path: string; auth: boolean };
const routes = {
home: { path: "/", auth: false },
admin: { path: "/admin", auth: true },
} as const satisfies Record<string, Route>;
routes.home.path; // "/"(字面量保留)as const satisfies T 串用,一步把值冻到最窄字面量类型并对照 T 校验。既拿到深只读字面量,又有形状检查。
function on<E extends string>(event: E) { return event; }
on("click"); // E 推断为 string
on("click" as const); // E 推断为 "click"
const ev = { type: "click" } as const;
ev.type; // "click",不是 string对字面量或对象用 as const,把属性冻到精确值并防止调用点放宽。这是把精确字面量传进泛型的轻量办法。
let value!: number; // 告诉编译器:我保证用前会赋值
function init() { value = 42; }
init();
value.toFixed(2); // ✅ 不再报 used before assigned
class C {
ref!: HTMLElement; // 类字段同理(由 DI / 生命周期赋值)
}变量或类字段名后加 ! 是明确赋值断言,告诉编译器使用前一定会被赋值。常用于由依赖注入、生命周期钩子或测试 setup 赋值的字段。
function dispatch(...args: readonly [string, number]) {}
const payload = ["save", 1] as const;
dispatch(...payload); // ✅ as const 让它是 readonly ["save", 1]
// 不加 as const 会推成 (string | number)[],spread 不匹配把数组 spread 进元组参数,需要数组是定长元组类型,as const 正好给出。不加它数组会放宽成 T[],spread 对不上位置。
interface User { id: number }
interface User { name: string }
// 自动合并:
// interface User { id: number; name: string }
const u: User = { id: 1, name: "Lei" };同作用域两个同名 interface 自动合并。库通过这个机制让用户扩展类型。
// my-app.d.ts
import "express";
declare module "express" {
interface Request {
user?: { id: number; name: string };
}
}
// 现在 Express 的 Request 上有了 user 字段
app.use((req, res, next) => {
req.user = { id: 1, name: "Lei" };
next();
});declare module "name" 重新打开外部模块加类型。给 Express 的 Request、Vue 的 ComponentCustomProperties 加字段的标准做法。
// globals.d.ts
declare global {
interface Window {
myApp: { version: string };
}
var __DEV__: boolean;
}
export {}; // 重要:让这个文件成为 module
// 使用
window.myApp = { version: "1.0.0" };
if (__DEV__) console.log("dev mode");module 里的 declare global { ... } 给全局作用域加类型。文件末尾 export {} 必加,让文件变成 module。构建期 define 和 Window 增强常用。
function area(r: number) { return Math.PI * r * r; }
namespace area {
export const unit = "cm²";
}
area(2); // 函数调用
area.unit; // "cm²",挂在同名 namespace 上函数与同名 namespace 合并,让你给函数挂上静态 helper 和常量。库就是这样在完整类型下同时暴露 fn 和 fn.helper。
enum Color { Red, Green, Blue }
namespace Color {
export function hex(c: Color): string {
return ["#f00", "#0f0", "#00f"][c];
}
}
Color.hex(Color.Red); // "#f00"与 enum 同名的 namespace 会合并,让你给枚举对象挂上 helper 函数。enum 仍既是值又是类型,同时多了方法。
// 描述一个由 <script> 注入、没有类型的全局
declare const ANALYTICS_ID: string;
declare function track(event: string): void;
track("page_view"); // ✅ 有类型,无运行时代码declare 为运行时存在但没有 TS 源码的东西(比如 script 注入的全局)引入纯类型声明。它不产出任何 JavaScript。
// env.d.ts
declare global {
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
}
export {};
import.meta.env.VITE_API_URL; // ✅ 有类型在 declare global 里重新打开一个全局 interface,可给现有环境类型(如 Vite 的 ImportMetaEnv)加字段。末尾 export {} 让文件保持 module,增强才正确生效。
// tsconfig: { "experimentalDecorators": true }
function logged<T extends { new(...args: any[]): {} }>(Cls: T) {
return class extends Cls {
constructor(...args: any[]) {
console.log("creating", Cls.name);
super(...args);
}
};
}
@logged
class User {
constructor(public name: string) {}
}
new User("Lei"); // logs "creating User"旧装饰器语法需要在 tsconfig 里开 experimentalDecorators: true。Angular / NestJS / TypeORM 仍在大量使用。
// 不需要 experimentalDecorators
function logged<C extends new (...args: any[]) => any>(
Cls: C,
ctx: ClassDecoratorContext,
) {
return class extends Cls {
constructor(...args: any[]) {
console.log("creating", ctx.name);
super(...args);
}
};
}
@logged
class User {
constructor(public name: string) {}
}TypeScript 5.0 实现 Stage 3 ECMAScript 装饰器提案,形状与旧版不同,多了 context 对象。新代码该用这个。
// Stage 3
function timed(
fn: Function,
ctx: ClassMethodDecoratorContext,
) {
return function (this: unknown, ...args: any[]) {
const t0 = performance.now();
const result = fn.apply(this, args);
console.log(`${String(ctx.name)} took ${performance.now() - t0}ms`);
return result;
};
}
class Api {
@timed
fetch() { /* ... */ }
}方法装饰器包装或替换方法。context 提供方法名、是否 static、access 等元数据。
function logged<T>(
target: { get: () => T },
ctx: ClassGetterDecoratorContext,
) {
return function (this: unknown) {
console.log("read", String(ctx.name));
return target.get.call(this);
};
}
class Box {
#v = 1;
@logged get value() { return this.#v; }
}getter 装饰器收到 { get } 和一个 ClassGetterDecoratorContext,可返回替换后的访问器。Stage 3 装饰器按目标种类各有不同的 context 类型。
function double(
_target: undefined,
ctx: ClassFieldDecoratorContext,
) {
return function (initial: number) {
return initial * 2; // 改写初始值
};
}
class C {
@double count = 5; // 实例化后 count === 10
}Stage 3 字段装饰器返回一个初始化函数,接收原始值并返回实际存入的值。它对每个实例运行,可变换或校验字段初始值。
function bound(
fn: Function,
ctx: ClassMethodDecoratorContext,
) {
ctx.addInitializer(function (this: any) {
this[ctx.name] = fn.bind(this); // 实例化时绑定 this
});
}
class Btn {
label = "ok";
@bound onClick() { return this.label; }
}Stage 3 的 context 提供 addInitializer,一个在构造期运行的回调。在里面把方法绑到 this,无需写 constructor 就实现了经典的自动绑定。
// ❌ any 把检查全关了,错误悄悄溜进运行时
function badParse(input: string): any {
return JSON.parse(input);
}
const data = badParse("{}");
data.foo.bar.baz(); // 不报错,运行时 TypeError
// ✅ unknown 强制你先收窄
function goodParse(input: string): unknown {
return JSON.parse(input);
}
const safe = goodParse("{}");
// safe.foo; ❌ 不让你动
if (typeof safe === "object" && safe !== null && "foo" in safe) {
// 这里才安全
}any 把类型检查全关了;unknown 强制收窄。"形状未知的值"(解析 JSON / 第三方回调)一律用 unknown,any 99% 的场景都是错的默认。
// ❌ 滥用 ! 把编译期检查关了
const el = document.getElementById("missing")!;
el.click(); // 运行时 Cannot read 'click' of null
// ✅ 优先显式 null 检查
const el2 = document.getElementById("input");
if (el2) el2.click();
// ✅ 优先可选链
el2?.click();后缀 ! 只在编译期去掉 null/undefined,运行时不变。断错了就是生产事故。优先 if (x) 或 x?.method()。
// ❌ 库的公共 API 用 type,消费者不能 declare merge 扩展
type LibUser = { id: number; name: string };
// ✅ 库的公共 API 用 interface
interface LibUser2 {
id: number;
name: string;
}
// 消费方:
declare module "lib" {
interface LibUser2 {
customField?: string;
}
}库的公共 API 消费者可能要扩展时用 interface(声明合并能用)。联合、元组、映射、条件类型必须 type 别名。
// 默认 method 形式参数是 bivariant,不严格
interface Listener {
onEvent(e: Event): void;
}
const l: Listener = {
onEvent(e: MouseEvent) {} // ✅ 默认允许(不安全)
};
// 开 strictFunctionTypes 后 function 形式参数严格逆变
type Listener2 = {
onEvent: (e: Event) => void;
};
const l2: Listener2 = {
// onEvent: (e: MouseEvent) => {} // ❌ 报错
onEvent: (e: Event) => {} // ✅
};方法简写(f(): T)是双变;函数属性(f: () => T)在 strictFunctionTypes 下严格逆变。事件处理类型用函数属性形式更安全。
// ❌ enum 会生成运行时代码(双向映射对象)
enum Color { Red, Green, Blue }
// ⚠️ const enum 内联,没运行时代码,但与 isolatedModules / babel 冲突
const enum Status { Ok, Err }
// ✅ 字符串字面量联合:零运行时、JSON 友好、自动补全完美
type ColorLit = "red" | "green" | "blue";普通 enum 会生成运行时对象(多打包字节)。const enum 内联但与 isolatedModules 冲突。字符串字面量联合是现代推荐,零成本、JSON 友好、自动补全完美。
// ❌ as 是不检查的强转
const x = "hello" as unknown as number;
const y = x.toFixed(2); // 编译过,运行时 TypeError
// ✅ 用 satisfies 校验
const config = {
port: 8080,
host: "localhost",
} satisfies Record<string, string | number>;
// ✅ 真不得已用 as,加注释说明原因
// DOM 保证返回 input,querySelector 不知道
const input = document.querySelector(".x") as HTMLInputElement;as 绕开编译器检查。校验用 satisfies;as 只在你真的比编译器知道得多时(DOM 查询)才用,并加注释说明原因。
interface User { id: number; name: string }
function merge(base: Partial<User>, override: Partial<User>): User {
// ❌ TS 不知道合并后必填字段都齐了
// return { ...base, ...override };
// ✅ 保证必填字段
return {
id: override.id ?? base.id ?? 0,
name: override.name ?? base.name ?? "",
...base,
...override,
};
}两个 Partial<T> spread 出来还是 Partial<T>,编译器无法证明必填字段都在。先给必填 key 显式兜底再 spread。
// 参数位置的 void 返回,接受任意返回值 type Handler = (e: Event) => void; const h: Handler = (e) => 42; // ✅ 合法,返回值被丢弃 // 这是有意为之,让 Array.prototype.forEach 等接受 (x => doSomething()) [1, 2, 3].forEach((x) => x + 1); // x + 1 返回 number,但 forEach 期望 void
参数位置 void 返回类型接受任意返回值。这是有意设计,让 forEach 等期望 void 的 callback 能接收任意表达式 arrow。知道这个怪癖即可,不一定是 bug。
interface Opts { width: number }
// ❌ 直接传字面量触发多余属性检查
// draw({ width: 10, height: 20 });
// ⚠️ 经过变量就绕过了检查(结构兼容)
const o = { width: 10, height: 20 };
draw(o); // ✅ 不报错
function draw(_: Opts) {}多余属性检查只在你直接传对象字面量时触发。先赋给变量就退回结构兼容,多出来的属性会悄悄通过。
const xs = [1, 2, 3]; const x = xs[10]; // 类型是 number,但运行时是 undefined! // ✅ 开 noUncheckedIndexedAccess 后: // x 的类型变成 number | undefined
默认情况下 arr[i] 即便越界也标成元素类型,把真实的 undefined 藏起来。开启 noUncheckedIndexedAccess 后,下标访问返回 T | undefined。
// {} 不是"空对象",而是"除 null/undefined 外的任何值"
let a: {} = 42; // ✅
let b: {} = "x"; // ✅
let c: {} = [1, 2]; // ✅
// let d: {} = null; ❌
// 想表达"任意非空对象"用 Record<string, unknown> 或 object类型 {} 表示"除 null 和 undefined 外的任何值",连数字、字符串都收,不只是对象。要真正表达对象用 object 或 Record<string, unknown>。
async function check() {
// ❌ Promise 永远是 truthy,分支必走
if (isReady()) { /* 总是进来 */ }
// ✅ 别忘了 await
if (await isReady()) { /* 正确 */ }
}
declare function isReady(): Promise<boolean>;Promise 对象永远是 truthy,所以 if 里没 await 的 promise 永远进分支。开启 typescript-eslint 的 no-misused-promises 能抓到这类 bug。
// == 会做类型转换,结果反直觉
// 0 == "" // true
// null == undefined // true
// [] == false // true
// ✅ 永远用 ===
if (value === 0) { /* ... */ }宽松的 == 会做隐式转换,得到 0 == "" 这种反直觉的相等。永远用严格的 ===;eqeqeq 这条 lint 规则可在全项目强制。
// 返回值被推宽成 string,丢了字面量
function makeBad() {
return { status: "ok" }; // { status: string }
}
// ✅ as const 或显式返回类型保留字面量
function makeGood() {
return { status: "ok" } as const; // { readonly status: "ok" }
}返回的对象字面量会放宽属性类型("ok" 变成 string),破坏下游可辨识联合的收窄。用 as const 或显式返回类型保留字面量。
try {
risky();
} catch (e) {
// e: unknown(useUnknownInCatchVariables 默认开)
// e.message; ❌ 不能直接访问
if (e instanceof Error) {
console.log(e.message); // ✅ 收窄后可用
}
}4.4 起 catch 变量被标成 unknown,因为可以 throw 任何东西,不止 Error。访问 .message 或 .stack 前先用 instanceof Error 收窄。
// ❌ 过度可选链把本不该缺的字段也容忍了 const name = response?.data?.user?.name ?? "anon"; // 如果 data 一定存在,缺了应该报错而不是默默 "anon" // ✅ 只在真正可空的环节用 ?. const name2 = response.data.user?.name ?? "anon";
在本该一定存在的字段上层层 ?.,会把结构性 bug 藏在一个静默兜底后面。只在真正可空的环节用可选链,别当成无脑护身符。
interface Dict { [k: string]: number }
const d: Dict = { count: 1 };
d.conut; // ⚠️ 不报错,类型 number,拼错也查不出
// ✅ 已知键用具体 interface,只有真动态才上索引签名字符串索引签名让任意属性访问都过类型检查,于是 d.conut 这种拼写错误也会作为值类型悄悄通过。索引签名只留给真正动态的 key,已知键显式列出来。
可搜索的 TypeScript 速查表,覆盖日常真在撸的 100+ 段 地道写法,不是凑数的 let x: number = 1 入门列表。十四 大分类:基础(string / number / boolean / null / undefined / never / unknown / any / void、字面量类 型、只读数组、元组),接口 vs 类型别名(什么时候挑哪 个、声明合并、extends vs 交叉、索引签名、调用签名), 联合与交叉(A | B 与 A & B、分配律、共同属性访问、 可辨识联合),泛型(单参 + 多参、extends 约束、默认 参数、泛型 + keyof 的组合约束、泛型函数 vs 泛型类型、 泛型 React 组件),工具类型(Partial、Required、 Readonly、Pick、Omit、Record、Exclude、Extract、 NonNullable、ReturnType、Parameters、Awaited、 ConstructorParameters、InstanceType、ThisParameterType、 OmitThisParameter、Uppercase / Lowercase / Capitalize), 类型收窄(typeof、instanceof、in、相等收窄、真值收 窄、用户自定义类型守卫 is、断言函数 asserts、可辨识 联合 switch 配 never 穷举检查),映射类型(同态映射、 用 as 重命名 key、+ 与 - 修饰 optional / readonly), 条件类型(T extends U ? X : Y、分配律条件类型、裸类 型参数 vs 包裹类型参数、条件类型链),infer 推断 (函数 ReturnType、Promise Awaited、元组头/尾、数组元 素、模板字面量捕获),模板字面量类型(4.1+,与联合 做交叉得到笛卡尔积、递归字符串解析),断言(as Foo、 as const 冻结字面量、satisfies 4.9+ 编译期校验不放宽 类型),声明合并(interface 自动合并、模块增强、全局 声明),装饰器(旧的实验装饰器 vs Stage 3 标准装饰 器、class / method / accessor / field),以及 7 个真 烧时间的坑(any vs unknown 一念之差全失守、非空 ! 滥用线上炸 null、type vs interface 选错命名后期改、 函数参数协变 / 逆变 / 双变看 strictFunctionTypes、 enum 运行时成本 vs const enum vs 字符串字面量联合、 as Foo 不安全断言把错误藏起来、void 返回值放宽接受 任意回调)。每条都带:双语标题、可直接复制的真实代 码、双语说明。搜索框跨标题 / 代码 / 说明三字段一起 过滤,分类胶囊缩范围,一键复制。完全在浏览器里跑, 不连任何服务、不上传。配合 Python / SQL / Vim / Regex 速查覆盖整条技术栈,搭 JSON Formatter 处理 数据。
把内容粘贴或拖入工具面板。
点击按钮,在浏览器内本地处理,文件不上传。
一键复制结果或下载到本地。
适合穿插在写代码、查问题、做 Review、上线前的小任务里。
这些入口会把当前任务接到更完整的工具链里。
你审一个 400 行的 PR,发现同事手写了一个跟 Partial 一模一样的 mapped type,没用内置的。你筛到「工具 类型」,复制 Pick、Omit、Partial 三段,留三条行内 评论,把 25 行自定义类型换成标准库一行写法。PR 体 积缩小,diff 也重新变得能读。
你给一个 fetch hook 建模 idle / loading / success / error 四个状态,老在 error 分支上误访问 .data。筛到 「联合与交叉」,复制可辨识联合加 never 穷举 switch, 编译器立刻拒掉你漏处理的任何分支。12 行的套路一次 性干掉一整类运行时 undefined 报错。
同事的接口返回 Promise<User[]>,你只想拿 User 又不 想 import。搜「infer」,复制 Awaited 配 infer 的条件 类型,写 ElementOf<Awaited<ReturnType<typeof fetchUsers>>>。不加新 import、不写 any,接口形状以 后改了类型还能自动跟着变。
你的主题配置有 40 个颜色 key,想让编译器在拼错时报 错,同时保留每个 key 的字面量类型给自动补全用。复制 satisfies 那段,把 `as const` 换成 `satisfies Theme`, 于是拼成 「primry」会直接挂掉构建,而 `theme.primary` 下游照样推断成那个精确的 hex 字符串。
想表达 unknown 却随手用 any 把报错压下去。any 会把下游所有检查全关掉,unknown 逼你先 typeof 或走守卫再用。复制 unknown 那段做收窄,别图省事。
用 as Foo 来「修」类型错误。强制转换不做校验,只是把不匹配藏到运行时才炸。校验优先用 satisfies,只有当你真比编译器知道得多(比如确定是某个 DOM 元素)才用 as。
联合或推导出来的形状却选了 interface,然后一路跟它较劲。interface 表达不了联合、元组、条件类型。一旦超出平铺对象形状,立刻换成 type 别名。
全部在你浏览器里跑。速查是单个静态页,搜索只对内存里的片段 数组做过滤,你输入的关键词不会离开标签页,也不会写进 URL。 没有代码执行、没有上传、没有任何网络请求。公司代理后面或气 隙机器上都能放心用。
做你这行的人, 还会一起用这些。