TypeScript 知识点补充(第三部分:工程化与高级类型)

TypeScript 知识点补充(第三部分:工程化与高级类型)

本部分是前两部分的延续。前面已覆盖「基础坑」「进阶坑」「实战模式」,本部分聚焦工程化配置、模块/声明文件、类型体操底层原理与框架实战。


一、模块与声明文件

1. 给无类型的第三方库补类型(.d.ts)

场景 :装了个老库 legacy-lib,没有任何类型提示。

ts 复制代码
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
  export function init(opts: { debug?: boolean }): void;
  const _default: { version: string };
  export default _default;
}

要点:

  • declare module 只声明"形状",不产生运行时代码。
  • 优先在 tsconfig 的 include 或 typeRoots 里收录该声明目录。
  • 能装 @types/xxx 就别自己写。

2. 全局类型扩展(declare global)

场景 :给 window、Express Request 等加自定义字段。

ts 复制代码
declare global {
  interface Window {
    __APP_BOOT__: { token: string } | undefined;
  }
}
export {}; // 必须有 import/export,否则文件被视为全局脚本而非模块

要点:

  • 末尾的 export {} 是关键,缺了它 declare global 会报错。
  • 利用 interface 同名合并特性扩展,而不是重新声明。

3. ESM 与 CommonJS 互操作坑

现象 :import express from 'express' 报"没有默认导出",但运行时明明能用。

jsonc 复制代码
// 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)。

ts 复制代码
type Animal = { name: string };
type Dog = { name: string; breed: string };

type Handler = (a: Animal) => void;

// 参数逆变:处理 Dog 的函数不能当"处理任意 Animal 的函数"用
const onlyDog: Handler = (d: Dog) => console.log(d.breed); // strictFunctionTypes 下报错

// 反过来:处理 Animal 的函数可以安全处理 Dog
const anyAnimal: Handler = (a: Animal) => console.log(a.name);

要点:

  • 方法定义语法(m(a: T): void)走双变 ,宽松;函数属性语法(m: (a: T) => void)走逆变,严格。
  • 想拿到严格检查,就写函数属性形式 + 开 strictFunctionTypes。

四、类型体操底层原理

1. 映射类型键重映射(as)

场景:给对象所有 key 加前缀 / 转 camelCase。

ts 复制代码
type AddPrefix<T, P extends string> = {
  [K in keyof T as `${P}${K & string}`]: T[K];
};

type R = AddPrefix<{ id: number; name: string }, 'x_'>;
// { x_id: number; x_name: string }

要点 :in keyof T as ... 的 as 子句可重命名甚至用 never 删除某个 key。

2. 工具类型手写(理解优于记忆)

ts 复制代码
type MyPartial<T>  = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyExclude<T, U> = T extends U ? never : T; // 分布式
type MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T;

要点 :Pick/Omit/Record/Partial/Required 全部基于「映射类型 + 条件类型 + infer」三件套。

3. 分布式条件类型陷阱

现象 :type T = string extends string ? 1 : 0; 得到 1,但写成泛型 Foo<string> 时会"分发"。

ts 复制代码
type IsAny<T> = 0 extends (1 & T) ? true : false; // 判断 any 的经典技巧

要点 :裸类型参数(naked type parameter)进入条件类型才会分发;不想分发用 [T] extends [U] 包一层。

4. 递归深度限制与尾递归优化

ts 复制代码
// 深层嵌套类型可能报 "Type instantiation is excessively deep"
// TS 4.8+ 对特定线性递归(如 MyAwaited、元组拼接)做尾递归优化,可撑到 ~1000 层

要点:

  • 递归类型尽量写成"线性递推"以命中尾递归优化。
  • 实在超限就拆分类型、或局部用宽类型兜底。

五、控制流分析的局限(容易误判的地方)

1. 闭包内收窄会丢失

ts 复制代码
let n: string | number = getValue();
if (typeof n === "string") {
  setTimeout(() => {
    n.toUpperCase(); // 报错:闭包内 TS 无法保证 n 没被改
  });
}

解决 :用 const 拷贝一份再在闭包用,或重新做一次 typeof 判断。

2. let 重新赋值打断收窄

ts 复制代码
let x: string | null = null;
if (x) {
  use(x);      // 这里 x 是 string
  x = null;    // 重新赋值后
  use2(x);     // 又回到 string | null,之前收窄失效
}

六、框架实战类型

React

ts 复制代码
type CardProps = {
  title: string;
  onSelect?: (id: string) => void;
};

// React 18 起 children 移出 FC 内置,不必显式指定
function Card({ title, onSelect }: CardProps) {
  return <button onClick={() => onSelect?.("id")} aria-label={title} />;
}

// 受控事件类型:ChangeEvent 泛型来自元素
import type { ChangeEvent } from "react";
const onChange = (e: ChangeEvent<HTMLInputElement>) => e.target.value;

Vue 3

ts 复制代码
// 泛型 defineProps + withDefaults 给默认值
const props = withDefaults(
  defineProps<{ id: string; count?: number }>(),
  { count: 0 }
);

// emits 类型化
const emit = 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 是"编译器帮我查",优先后者。
相关推荐
10岁的博客2 小时前
Chrome 扩展安全攻防实录:从权限模型到真实漏洞复盘
前端·chrome·安全
QianhangQianping2 小时前
WebSocket实时推送+ HTTP 定时轮询:一块大屏两套取数怎么不打架
前端·websocket·网络协议·http·低代码·架构·数据可视化
Eider19982 小时前
Python基础知识之并发相关-2
开发语言·前端·python
用户69371750013842 小时前
AI 能写代码,但工程架构还是应该由人来定
前端·后端
Hilaku2 小时前
前端的技术判断力是怎么练出来的?
前端·javascript·程序员
悠仁さん2 小时前
【C++】set map 使用
java·前端·c++
用户093671755412 小时前
用 4500 行 TypeScript 复刻了童年方格纸上的海战棋
前端·vue.js
CCIE-Yasuo2 小时前
网络安全-学习web开发-基础概念、常用标签
前端·学习
凉介川2 小时前
给 DeepSeek Harness 写插件:按扩展名接管 Windows 右键打开
前端