楼层: 首页/ 软件技术/ 前端基础/ 第 5 章 TypeScript 5:给 JS 上安全带
型

第 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 写死就别炫技。