☰
TypeScript 知识点补充(第三部分:工程化与高级类型)
2026/11/2 11:17:55 网站建设 项目流程

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其实是多个开关的集合,逐项了解才能精准放开:

开关作用常见取舍
strictNullChecksnull/undefined不再可赋给任意类型核心,必开
noImplicitAny禁止隐式 any必开
strictFunctionTypes函数参数逆变检查(见下)建议开
strictBindCallApplybind/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。


防坑总口诀(最终版)

  1. 值用import、类型用import type,导入导出死磕带type。
  2. 能不写any就不写;非黑盒不可用时优先unknown+ 类型守卫收窄。
  3. 字面量怕被放宽,用as const或satisfies。
  4. 接口数据 TS 类型不算数,必须 Zod / 运行时校验兜底。
  5. 泛型访问属性先extends约束;从结构里抠类型用infer。
  6. switch联合类型用never兜底,漏分支立刻报错。
  7. strict系列 +noUncheckedIndexedAccess长期开启,越界与隐式 any 早发现。
  8. 断言(as)是“我负责”,注解/satisfies 是“编译器帮我查”,优先后者。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询