TypeScript 知识点补充(第三部分:工程化与高级类型)
本部分是前两部分的延续。前面已覆盖「基础坑」「进阶坑」「实战模式」,本部分聚焦工程化配置、模块/声明文件、类型体操底层原理与框架实战。
一、模块与声明文件
1. 给无类型的第三方库补类型(.d.ts)
场景:装了个老库legacy-lib,没有任何类型提示。
// types/legacy-lib.d.tsdeclaremodule'legacy-lib'{exportfunctioninit(opts:{debug?:boolean}):void;const_default:{version:string};exportdefault_default;}要点:
declare module只声明“形状”,不产生运行时代码。- 优先在
tsconfig的include或typeRoots里收录该声明目录。 - 能装
@types/xxx就别自己写。
2. 全局类型扩展(declare global)
场景:给window、Express Request等加自定义字段。
declareglobal{interfaceWindow{__APP_BOOT__:{token:string}|undefined;}}export{};// 必须有 import/export,否则文件被视为全局脚本而非模块要点:
- 末尾的
export {}是关键,缺了它declare global会报错。 - 利用
interface同名合并特性扩展,而不是重新声明。
3. ESM 与 CommonJS 互操作坑
现象:import express from 'express'报“没有默认导出”,但运行时明明能用。
// tsconfig.json { "compilerOptions": { "esModuleInterop": true, // 允许对 CJS 模块用默认导入语法 "allowSyntheticDefaultImports": true, "module": "NodeNext", "moduleResolution": "NodeNext" } }要点:
esModuleInterop解决的是“CJS 被当默认导入”的语法兼容。module与moduleResolution必须成对配置,版本不匹配会大量报“找不到模块”。
二、tsconfig 严格模式子开关拆解
"strict": true其实是多个开关的集合,逐项了解才能精准放开:
| 开关 | 作用 | 常见取舍 |
|---|---|---|
strictNullChecks | null/undefined不再可赋给任意类型 | 核心,必开 |
noImplicitAny | 禁止隐式 any | 必开 |
strictFunctionTypes | 函数参数逆变检查(见下) | 建议开 |
strictBindCallApply | bind/call/apply参数检查 | 建议开 |
noImplicitThis | 禁止隐式 this 类型 | 建议开 |
alwaysStrict | 自动按"use strict"编译 | 开 |
额外推荐:
noUncheckedIndexedAccess:索引访问返回T | undefined,防越界。exactOptionalPropertyTypes:可选属性不能显式赋undefined,区分“没传”与“传了 undefined”。noImplicitReturns:函数不能有的分支返回值、有的不返回。
三、协变与逆变(函数兼容性的根)
核心结论:函数参数是逆变(contravariant),返回值是协变(covariant)。
typeAnimal={name:string};typeDog={name:string;breed:string};typeHandler=(a:Animal)=>void;// 参数逆变:处理 Dog 的函数不能当“处理任意 Animal 的函数”用constonlyDog:Handler=(d:Dog)=>console.log(d.breed);// strictFunctionTypes 下报错// 反过来:处理 Animal 的函数可以安全处理 DogconstanyAnimal:Handler=(a:Animal)=>console.log(a.name);要点:
- 方法定义语法(
m(a: T): void)走双变,宽松;函数属性语法(m: (a: T) => void)走逆变,严格。 - 想拿到严格检查,就写函数属性形式 + 开
strictFunctionTypes。
四、类型体操底层原理
1. 映射类型键重映射(as)
场景:给对象所有 key 加前缀 / 转 camelCase。
typeAddPrefix<T,Pextendsstring>={[KinkeyofTas`${P}${K&string}`]:T[K];};typeR=AddPrefix<{id:number;name:string},'x_'>;// { x_id: number; x_name: string }要点:in keyof T as ...的as子句可重命名甚至用never删除某个 key。
2. 工具类型手写(理解优于记忆)
typeMyPartial<T>={[KinkeyofT]?:T[K]};typeMyReadonly<T>={readonly[KinkeyofT]:T[K]};typeMyPick<T,KextendskeyofT>={[PinK]:T[P]};typeMyExclude<T,U>=TextendsU?never:T;// 分布式typeMyAwaited<T>=TextendsPromise<inferV>?MyAwaited<V>:T;要点:Pick/Omit/Record/Partial/Required全部基于「映射类型 + 条件类型 + infer」三件套。
3. 分布式条件类型陷阱
现象:type T = string extends string ? 1 : 0;得到1,但写成泛型Foo<string>时会“分发”。
typeIsAny<T>=0extends(1&T)?true:false;// 判断 any 的经典技巧要点:裸类型参数(naked type parameter)进入条件类型才会分发;不想分发用[T] extends [U]包一层。
4. 递归深度限制与尾递归优化
// 深层嵌套类型可能报 “Type instantiation is excessively deep”// TS 4.8+ 对特定线性递归(如 MyAwaited、元组拼接)做尾递归优化,可撑到 ~1000 层要点:
- 递归类型尽量写成“线性递推”以命中尾递归优化。
- 实在超限就拆分类型、或局部用宽类型兜底。
五、控制流分析的局限(容易误判的地方)
1. 闭包内收窄会丢失
letn:string|number=getValue();if(typeofn==="string"){setTimeout(()=>{n.toUpperCase();// 报错:闭包内 TS 无法保证 n 没被改});}解决:用const拷贝一份再在闭包用,或重新做一次typeof判断。
2.let重新赋值打断收窄
letx:string|null=null;if(x){use(x);// 这里 x 是 stringx=null;// 重新赋值后use2(x);// 又回到 string | null,之前收窄失效}六、框架实战类型
React
typeCardProps={title:string;onSelect?:(id:string)=>void;};// React 18 起 children 移出 FC 内置,不必显式指定functionCard({title,onSelect}:CardProps){return<button onClick={()=>onSelect?.("id")}aria-label={title}/>;}// 受控事件类型:ChangeEvent 泛型来自元素importtype{ChangeEvent}from"react";constonChange=(e:ChangeEvent<HTMLInputElement>)=>e.target.value;Vue 3
// 泛型 defineProps + withDefaults 给默认值constprops=withDefaults(defineProps<{id:string;count?:number}>(),{count:0});// emits 类型化constemit=defineEmits<{change:[value:string,id:number]}>();emit("change","a",1);要点:现代框架推荐泛型宏 + 字面量,避免把 props 写成any或Record<string, unknown>。
七、类型断言 / satisfies / 注解 三选一决策
| 写法 | 何时用 | 风险 |
|---|---|---|
const x: T = ...(注解) | 你知道确切类型,想约束并得到检查 | 可能把窄类型放宽 |
x satisfies T | 想验证符合 T,但保留更精确的推断类型 | 仅 TS 4.9+ |
x as T(断言) | 你比编译器更了解运行时(接口/DOM 取元素) | 不做检查,写错不报错 |
口诀:能用注解就别断言;要保留字面量信息用satisfies;只有“骗编译器”时才as。
防坑总口诀(最终版)
- 值用
import、类型用import type,导入导出死磕带type。- 能不写
any就不写;非黑盒不可用时优先unknown+ 类型守卫收窄。- 字面量怕被放宽,用
as const或satisfies。- 接口数据 TS 类型不算数,必须 Zod / 运行时校验兜底。
- 泛型访问属性先
extends约束;从结构里抠类型用infer。switch联合类型用never兜底,漏分支立刻报错。strict系列 +noUncheckedIndexedAccess长期开启,越界与隐式 any 早发现。- 断言(
as)是“我负责”,注解/satisfies 是“编译器帮我查”,优先后者。