型
第 5 章 TypeScript 5:给 JS 上安全带
TypeScript — Types on Top of JavaScript
后端同学写 Java/Rust 享受编译期类型检查,写 JS 时却要在运行时才发现"undefined 不是函数"。TypeScript 就是 JS 的超集,加了一层类型系统:写的时候就报错,不用跑到线上才炸。大项目、多人协作,不上 TS 几乎是裸奔。
论TS 的定位
TS 不会运行:它先把 .ts 编译成纯 .js,类型信息全部扔掉。所以浏览器跑的还是 JS。
类型推断很聪明:const x = 1,TS 自动知道 x 是 number,不用你写 const x: number = 1。别到处写注解,让 TS 推。
strict: true 必须开:tsconfig.json 里打开严格模式,null/undefined 都要显式处理,错误最少。
基本类型
let name: string = "小明";
let age: number = 18;
let ok: boolean = true;
let list: number[] = [1, 2, 3];
// 元组:固定长度、固定类型
let pair: [string, number] = ["id", 42];
// any:放弃类型检查,等于写 JS,尽量别用
let messy: any = 1;
messy.foo(); // TS 不报错,运行时炸
// unknown:更安全的 any,必须先收窄才能用
let safe: unknown = "hello";
// safe.length; // 报错!不知道是什么类型
if (typeof safe === "string") {
safe.length; // 收窄后合法
}
interface 与 type
// interface:描述对象的形状,可声明合并、可被类 implements
interface User {
id: number;
name: string;
age?: number; // ? 表示可选
readonly createdAt: Date; // readonly 只读
greet(): string; // 方法签名
}
// type 别名:更灵活,能写联合、交叉、工具类型
type ID = string | number;
type Status = "loading" | "success" | "error"; // 字面量联合,对应后端 enum
const u: User = {
id: 1, name: "小明", createdAt: new Date(),
greet() { return "你好"; }
};
怎么选?描述对象形状用 interface,写联合/交叉/工具类型用 type。社区里两者都很常见,不用纠结。
泛型:类型的参数
// 不用泛型:参数和返回值类型对不上
function identityBad(arg: any): any { return arg; }
// 用泛型 T:传什么类型,返回什么类型
function identity<T>(arg: T): T { return arg; }
const s = identity<string>("hi"); // s: string
// 泛型约束:T 必须有 length 属性
function withLength<T extends { length: number }>(x: T) {
return x.length;
}
// 工具类型:TS 的瑞士军刀
type PartialUser = Partial<User>; // 全部变可选
type UserPreview = Pick<User, "id" | "name">; // 只挑这两个
type UserWithoutPwd = Omit<User, "age">; // 去掉 age
type UserMap = Record<number, User>; // 数字键到 User 的映射
与 React 配合
// .tsx 文件里,组件 props 要写类型
type Props = { title: string; count?: number };
function Card({ title, count = 0 }: Props) {
// useState 泛型告诉它状态是什么类型
const [n, setN] = useState<number>(0);
// 事件类型从 React 导入
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
setN(n + 1);
};
return <button onClick={onClick}>{title}:{n}</button>;
}
TS 常见坑
① 滥用 any:写着写着全 any,等于没写 TS。报错了先想办法收窄类型,别一把 any 盖过去。
② 类型断言 as:x as Foo 是"我比编译器清楚",乱用会骗过类型检查。能用类型守卫就用守卫。
③ strict 没开:老项目默认 strict: false,null 满天飞。新项目必须开 true。
TypeScript 深化:类型系统全貌与工具类型
入门只会写 : string 不算会 TS。真正的威力在泛型、类型收窄、工具类型——它们让你用类型把业务模型"建模"出来,改字段时编译器替你查漏改。
函数类型与重载
// 参数类型、返回值类型、可选/默认/剩余参数
function greet(name: string, title?: string, ...others: string[]): string {
return `${title ?? "同学"},${name}:${others.join("/")}`;
}
// 函数类型作为参数:cb 必须是接收 number 返回 void 的函数
function runLater(cb: (n: number) => void) { cb(42); }
// 函数重载:同一个函数,不同入参有不同返回类型
function pad(n: number): string;
function pad(s: string): string;
function pad(x: number | string): string {
return String(x).padStart(3, "0");
}
联合 / 交叉 / 字面量 / 类型收窄
// 联合 |:二选一;交叉 &:两者都要
type Id = string | number;
type WithName = { name: string };
type WithAge = { age: number };
type Person = WithName & WithAge; // 两个字段都要有
// 字面量联合:精确建模"状态机"
type Status = "idle" | "loading" | "success" | "error";
// 可辨识联合 discriminated union:靠一个公共字段收窄
type Result =
| { status: "success"; data: User }
| { status: "error"; message: string };
function handle(r: Result) {
if (r.status === "success") return r.data; // 这里 r 有 data
return r.message; // 这里 r 有 message
}
// 类型守卫:is 谓词,自定义收窄函数
function isUser(x: unknown): x is User {
return (x as User)?.name !== undefined;
}
// 穷尽检查:default 分支 x: never,加新状态时编译器逼你补处理
function assertNever(x: never): never { throw new Error(x); }
泛型与工具类型全家桶
// 泛型约束:T extends 表示 T 必须满足某些条件
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // key 一定是 obj 的键,返回对应值的类型
}
// 条件类型 + infer:推断函数返回值 / 异步解包
type Awaited<T> = T extends Promise<infer U> ? U : T;
type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never;
// 内置工具类型(背熟这几个够 90% 场景)
type T1 = Partial<User>; // 所有属性变可选
type T2 = Required<User>; // 全部必填
type T3 = Readonly<User>; // 全部只读
type T4 = Pick<User, "id"|"name">; // 挑几个
type T5 = Omit<User, "password">; // 去掉几个
type T6 = Record<string, User>; // 字典:键→User
type T7 = Exclude<A, B>; // 从 A 去掉 B
type T8 = Extract<A, B>; // A 和 B 的交集
type T9 = NonNullable<T | null|undefined>; // 去掉 null/undefined
type T10 = Awaited<Promise<User>>; // User
枚举、类成员与装饰器
// 数字枚举:默认从 0 自增
enum Role { Admin, User, Guest }
// 字符串枚举:可读性好,推荐
enum Direction { Up = "UP", Down = "DOWN" }
// const enum:编译时内联,运行时完全不生成代码
const enum HttpStatus { Ok = 200, NotFound = 404 }
// class:私有字段 # 是 JS 原生私有,TS private 只是编译期检查
class Counter {
#count = 0; // 真私有
private label = "n"; // 仅编译期
static version = "1.0"; // 静态属性
get value() { return this.#count; }
inc() { this.#count++; }
}
// 装饰器(Stage 3 稳定化中):@Decorator 装饰类/方法/属性
// @deprecated 标记弃用;@memoize 缓存;框架(Nest/Midway)大量用
// 实验阶段需在 tsconfig 开 experimentalDecorators
tsconfig.json 关键项
{
"compilerOptions": {
"target": "ES2022", // 编译成哪个版本 JS
"module": "ESNext", // 模块系统
"moduleResolution": "bundler", // 配合 Vite 用 bundler 解析
"lib": ["ES2022", "DOM"], // 可用的类型库(浏览器 DOM 必须)
"jsx": "react-jsx", // React 17+ 自动运行时
"strict": true, // 总开关:下面一堆严格检查
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true, // 未使用变量报错
"noUnusedParameters": true,
"esModuleInterop": true, // 允许 import x from 'cjs-pkg'
"skipLibCheck": true, // 跳过 .d.ts 内部检查,编译快
"outDir": "./dist",
"sourceMap": true,
"declaration": true // 打库时输出 .d.ts
},
"include": ["src"]
}
第三方库没自带类型时装 @types/lodash 这种社区声明包;declare module "*.css" 用来告诉 TS import "./x.css" 是合法的。
完整案例:类型安全的 API 请求封装
// request.ts —— 泛型 + 接口 + 错误处理
interface ApiError { code: number; message: string }
class RequestError extends Error {
constructor(public payload: ApiError) { super(payload.message); }
}
// 泛型函数:传入 T,返回 Promise<T>,调用方拿到的就是精确类型
async function request<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, {
headers: { "Content-Type": "application/json" },
...init,
});
const body = await res.json() as T | ApiError;
if (!res.ok) throw new RequestError(body as ApiError);
return body as T;
}
// 业务层:定义接口,调用即类型安全
interface User { id: number; name: string; email: string }
const getUser = (id: number) =>
request<User>(`/api/users/${id}`);
// 用:u 自动推导为 User,写错字段立刻红线
// const u = await getUser(1); u.nmae // 报错
本章面试题(TypeScript)
1. interface 和 type 区别?什么时候用哪个?
看答案
interface 可声明合并、可被 class implements、可继承;type 更灵活,能写联合/交叉/条件/映射类型。社区惯例:描述对象形状、公共 API 契约用 interface;复杂类型运算、联合类型用 type。
2. 泛型解决什么问题?不用泛型会怎样?
看答案
泛型让类型像函数参数一样可传递。不用泛型就只能用 any,参数和返回值类型脱钩,调用方拿到 any 等于没类型。用了 <T>,传 string 返回 string,编译器全程跟踪。
3. Partial / Pick / Omit / Record 各干什么?
看答案
Partial 全可选;Pick 挑指定键;Omit 去掉指定键;Record 构造"键类型→值类型"的字典。它们都是映射类型,在对象类型上批量改形状。
4. as 类型断言和类型守卫的区别?为什么说断言不安全?
看答案
断言是"我比编译器清楚",强制把类型改成另一种,不做运行时检查,可能骗编译器;类型守卫是运行时真的判断 x is T,断言通过才收窄。能用守卫就别用断言。
5. strict 模式开了什么?为什么建议开?
看答案
strict 打开 noImplicitAny、strictNullChecks、strictFunctionTypes 等一堆严格检查。它逼你显式处理 null/undefined 和隐式 any,把大量运行时错误提前到编译期,长期收益巨大。新项目一律 true。
条件类型、infer 与分布式分发
TS 的类型系统是图灵完备的,最能体现这点的就是条件类型:T extends U ? X : Y——"如果 T 能赋值给 U,取 X,否则取 Y"。配合 infer 能"从类型里抠出另一块类型",写工具类型全靠它。
条件类型 + infer:把函数返回值、数组元素、Promise 结果"抠"出来
// ① 基本条件类型:T 是 string 吗?
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<123>; // false
// ② infer:在 extends 右侧"声明一个待推断的类型变量"
// 抠出函数的返回值类型(这就是内置 ReturnType 的原理)
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => number>; // number
// ③ 抠出数组元素类型
type Unpack<T> = T extends (infer E)[] ? E : T;
type E1 = Unpack<string[]>; // string
分布式条件类型(distributive):当 T 是联合类型时,条件类型会"分发"到每个成员上再合并:Exclude<string | number, number> 会变成 (string extends number ? never : string) | (number extends number ? never : number) = string | never = string。这就是内置 Exclude / Extract 的实现原理。
| 内置工具类型 | 干什么(底层都是条件类型 + infer) |
Partial<T> / Required<T> | 把所有属性变可选 / 必选。 |
Pick<T, K> / Omit<T, K> | 挑出部分属性 / 删掉部分属性。 |
Record<K, V> | 构造"键为 K、值为 V"的对象类型。 |
ReturnType<F> / Parameters<F> | 抠出函数返回值类型 / 参数元组类型。 |
Exclude<T, U> / Extract<T, U> | 从联合 T 中剔除 / 取出 与 U 重叠的成员。 |
Awaited<T> | 递归拆 Promise,拿到最终值类型。 |
什么时候自己写条件类型:大部分场景用内置工具类型就够;只有在"根据入参类型自动推导返回类型""事件名到回调签名的映射"这类库设计时,才需要手写 infer。业务代码别过度,能 interface 写死就别炫技。