TypeScript 的类型系统是结构化的:它比较的是类型的形状(有哪些成员),而不是类型的名字。理解这一点,大部分"为什么这里报错/不报错"就有答案了。
本文是 类型体操 的前置——先会读类型、写类型,再去玩推导。
interface Point { x: number; y: number }
class Vector { constructor(public x: number, public y: number) {} }
// Vector 没有 implements Point,但由于形状一致,可以直接赋值
const p: Point = new Vector(1, 2);
推论:只要成员齐全就能赋值,多余的成员在直接字面量赋值时会被拦截(多余属性检查)。
const a: Point = { x: 1, y: 2, z: 3 }; // ✗ 报错:对象字面量只能指定已知属性
const b = { x: 1, y: 2, z: 3 };
const c: Point = b; // ✓ 通过(b 不是字面量,不做多余属性检查)
如果需要"名义类型"的约束(比如区分 UserId 和 OrderId 都是 string 的情况),用品牌类型:
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
declare const uid: UserId;
declare const oid: OrderId;
// uid = oid; // ✗ 类型不兼容,避免了"两个 id 传反"的低级错误
| 类型 | 含义 | 能否赋值给别人 | 能否被赋值 |
|---|---|---|---|
any | 关闭检查 | 可以 | 可以 |
unknown | 未知,用前必须收窄 | 不可以 | 可以 |
never | 不可能存在的值 | 可以 | 只接受 never |
// unknown 是安全的 any:必须先收窄
function parse(input: unknown) {
if (typeof input === "string") return input.trim(); // 这里才被当作 string
return "";
}
// never 的两个实际用途
// 1. 穷尽检查
type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "rect": return s.w * s.h;
default: {
const _exhaustive: never = s; // 漏了分支这里会报错
return _exhaustive;
}
}
}
实战建议:只在迁移期用
any(配合noImplicitAny逐步收紧),新代码用unknown+ 收窄。
TypeScript 会沿着控制流分析变量的类型,这叫控制流分析(CFA)。
function format(v: string | number | Date | null) {
if (v === null) return "";
if (typeof v === "string") return v.trim(); // string
if (typeof v === "number") return v.toFixed(2); // number
return v.toISOString(); // Date(剩下的唯一类型)
}
常用的收窄手段:
| 手段 | 适用场景 |
|---|---|
typeof | 原始类型 |
instanceof | 类实例 |
in | 判断成员是否存在(联合对象) |
判别属性(kind) | 可辨识联合,最推荐 |
自定义守卫 x is T | 复杂结构,如接口返回 |
断言函数 asserts x is T | 失败即抛错 |
// 自定义守卫:注意返回值类型是 x is T,不是 boolean
function isUser(x: unknown): x is { id: string; name: string } {
return typeof x === "object" && x !== null && "id" in x && "name" in x;
}
// 断言函数:调用后整个作用域都收窄
function assertUser(x: unknown): asserts x is { id: string } {
if (!isUser(x)) throw new TypeError("not a user");
}
泛型是"类型的参数",核心价值是保留入参与返回值的类型关联。
// ✗ 用 any:丢失了关联
function firstAny(arr: any[]): any { return arr[0]; }
// ✓ 用泛型:T 被推断并保留
function first<T>(arr: T[]): T | undefined { return arr[0]; }
const s = first(["a", "b"]); // string | undefined
约束与默认值:
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
type ApiRes<T = unknown> = { code: number; data: T };
推断优先级:调用时显式传入 > 参数推断 > 默认值。
type IsArray<T> = T extends any[] ? true : false;
// infer 在 extends 里"提取"类型
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<number>>; // number
type B = ElementOf<string[]>; // string
分布式条件类型:当 T 是裸类型参数且传入联合类型时,会逐个分发。
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[](分发)
// 想避免分发:用元组包一层
type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNoDist<string | number>; // (string | number)[]
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 去掉 readonly
type Required<T> = { [K in keyof T]-?: T[K] }; // 去掉可选
// 键重映射(4.1+)
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
模板字面量类型让字符串也有了类型运算:
type EventName = "click" | "focus";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus"
| 类型 | 作用 |
|---|---|
Partial<T> / Required<T> | 全部可选 / 全部必填 |
Readonly<T> | 全部只读 |
Pick<T, K> / Omit<T, K> | 取子集 / 排除子集 |
Record<K, V> | 构造键值映射 |
Exclude<T, U> / Extract<T, U> | 联合类型减 / 取 |
NonNullable<T> | 去掉 null / undefined |
ReturnType<F> / Parameters<F> | 取返回值 / 参数元组 |
Awaited<T> | 递归解包 Promise |
实现原理见 类型体操。
tsconfig.json 里影响类型检查行为的几个关键项:
{
"compilerOptions": {
"strict": true, // 一次开启下面所有严格项,新项目必开
"noImplicitAny": true, // 禁止隐式 any(迁移老项目时最后再开)
"strictNullChecks": true, // null/undefined 不兼容其他类型
"noUncheckedIndexedAccess": true, // arr[i] 结果带 undefined,防止越界
"exactOptionalPropertyTypes": true, // 区分"没有该属性"与"值为 undefined"
"skipLibCheck": true, // 跳过 node_modules 的 d.ts 检查,显著提速
"moduleResolution": "bundler" // 配合 vite/webpack,无需写扩展名
}
}
声明文件:给没有类型的库写 .d.ts,或给全局对象扩展类型。
// types/global.d.ts
declare global {
interface Window { __APP_VERSION__: string }
}
export {};
as 不是类型转换——它只说服编译器,运行时没有任何检查。能用守卫就不要用断言。instanceof 无法用于 interface。Object.keys() 返回 string[],不是 keyof T。需要精确键时用 (Object.keys(o) as (keyof T)[])。any 加注释。interface 和 type 的区别?(合并能力 / 映射类型 / 联合类型)any 和 unknown 的区别?为什么推荐 unknown?