Skip to content

TypeScript

已收录:本页完成领域概览规划清单中 TypeScript 的 4 项内容(类型层级与类型收窄、条件/映射/模板字面量类型、类型体操实战、声明文件与第三方类型治理)。示例标注运行环境,可直接复制验证。

本页按「语法要点 → 运行机制 → 示例 → 踩坑」组织,覆盖类型系统与工程实践:

  1. 类型层级与类型收窄
  2. 条件类型、映射类型与模板字面量类型
  3. 类型体操实战
  4. 声明文件与第三方类型治理

类型层级与类型收窄

类型层级(语法要点)

TypeScript 的类型可以看作集合:一个值满足的类型越多,该类型越「宽」。层级从上到下:

unknown              ← 顶层类型(任何值都兼容)
├── any              ← 逃逸口(可赋给任何类型,也可收任何类型)
├── string | number  ← 联合类型(并集)
│   ├── string
│   └── number
├── ... 
└── never            ← 底层类型(空集,不可能有值)

核心判断:类型 A 能赋值给类型 B ⇔ A 是 B 的子类型(A ⊆ B)。unknown 只能收不能给(除 any);never 能给任何类型但不能被任何类型赋值;any 双向兼容,是类型检查的「逃生舱」。

ts
// 运行环境:tsc ≥ 4.5,Node.js ≥ 18(以下为类型注解,运行前请删除或使用 tsc --noEmit)
let u: unknown = 'anything'
u = 42            // ✅ unknown 能收任何值
// u.toFixed()    // ❌ 不能直接调用方法,需要收窄
let n: number = 1
// u = n 的反向不行:n = u // ❌ unknown 不能赋给 number

function f(x: never): never { throw new Error('不可达') }
// never 通常由控制流推导,无需手写

const a: any = '1'
const b: number = a  // ✅ any 可赋给任何类型(这也是 its 危害来源)

基础类型与结构化写法速查:

写法含义说明
string / number / boolean / bigint / symbol原始类型小写,勿与包装对象混用
string[] / Array<string>数组等价
[string, number]元组定长定序;readonly [string, number] 只读
{ a: number; b?: string }对象类型b? 可选
'a' | 'b'字面量联合常用作选项枚举
string | number联合并集
{ a: number } & { b: string }交叉交集:须同时满足
unknown / never / any特殊类型见上
keyof T / T[K]键集合 / 索引访问元编程基础

类型收窄(语法要点 + 运行机制)

联合类型在运行时只有一种形态,编译器需要借助控制流分析把宽类型「收窄」成窄类型才能安全使用:

ts
// 运行环境:tsc ≥ 4.5(tsc --noEmit 验证)
type Id = string | number
function getLen(id: Id) {
  // typeof 收窄:分支内 id 被推导为对应窄类型
  if (typeof id === 'string') return id.length
  return id // 此处剩 number
}

interface Circle { kind: 'circle'; r: number }
interface Square { kind: 'square'; side: number }
type Shape = Circle | Square

function area(s: Shape): number {
  // 可辨识联合:用字面量字段 kind 收窄(必须同名字段 + 字面量类型)
  switch (s.kind) {
    case 'circle': return Math.PI * s.r ** 2
    case 'square': return s.side * s.side
  }
}

其他收窄手段:

手段示例说明
typeoftypeof x === 'string'原始类型;注意 typeof null === 'object'
instanceofx instanceof Date类/构造实例
in'kind' in s判断对象是否含某键
truthinessif (x != null)排除 null/undefined 与假值
相等收窄x === 'ok'与字面量比较
可辨识联合s.kind === 'circle'最常用,见上
类型谓词isX(x): x is X自定义收窄(见下)
断言函数assertX(x): asserts x is X不通过即抛错,之后窄类型生效
Array.isArrayArray.isArray(x)数组收窄

自定义守卫函数(type predicate),写校验逻辑后把结果「翻译」给类型系统:

ts
// 运行环境:tsc ≥ 4.5
interface User { name: string; age: number }

function isUser(v: unknown): v is User {
  return !!v && typeof v === 'object' && 'name' in v && 'age' in v
}

function handle(data: unknown) {
  if (isUser(data)) {
    data.name.toUpperCase() // ✅ 收窄为 User,可安全访问
  }
}

as const 与只读对象:把对象字面量变成只读且值为字面量类型,是「无 enum 枚举」的推荐做法:

ts
// 运行环境:tsc ≥ 4.5
const Status = {
  Idle: 'idle',
  Loading: 'loading',
  Done: 'done'
} as const

type StatusValue = (typeof Status)[keyof typeof Status] // 'idle' | 'loading' | 'done'
function setStatus(s: StatusValue) { /* ... */ }
// setStatus('nope') // ❌ 类型报错

踩坑记录

  • any 会关闭类型检查并传染(函数返回 any,调用方跟着变 any):新代码显式标注 unknown,收窄后再用;
  • typeof null === 'object',用 v != nullv && typeof v === 'object' 判断对象;
  • enum 是值(运行时存在)且非结构化的:跨模块数值枚举与 const enum 有坑,可用 as const 对象替代;
  • 可选属性 b?: string 的类型是 string | undefined,区别:{ b?: string } 不含 b 合法,{ b: string | undefined } 必须显式给 b: undefined
  • tsc 默认 strict: false,很多坑被静默——新项目务必 strict: true
  • 数组用 Array<T> 而非 T[] 标注泛型时注意 readonly 数组与可变数组不能互赋。

条件类型、映射类型与模板字面量类型

条件类型与 infer(语法要点)

条件类型 = 类型层面的「三元表达式」:T extends U ? X : Y分发(distributive):当 T 是裸类型参数且为联合类型时,会对联合的每个成员分别判断再合并结果——这是很多工具类型的基石。

ts
// 运行环境:tsc ≥ 4.5(tsc --noEmit)
type IsString<T> = T extends string ? true : false
type R1 = IsString<'a'>            // true
type R2 = IsString<123>            // false
type R3 = IsString<'a' | 123>      // boolean ← 分发后得到 true | false 再合并

// 不想要分发时,把 T 包进元组
type NoDist<T> = [T] extends [string] ? true : false
type R4 = NoDist<'a' | 123>        // false(整体判断)

infer 从结构里「抽取」一个待推断类型,通常与条件类型搭配:

ts
// 运行环境:tsc ≥ 4.5
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type MyParameters<T> = T extends (...args: infer P) => any ? P : never
type MyAwaited<T> = T extends Promise<infer U> ? U : T

declare function load(): Promise<{ id: number }>
type Data = MyAwaited<ReturnType<typeof load>> // { id: number }(递归见类型体操)

内置工具类型(会手写即可理解):

工具类型作用手写要点
Partial<T>全部属性可选映射类型加 ?
Required<T>全部属性必选-?
Readonly<T>全部属性只读readonly
Pick<T, K>取部分键K extends keyof T 过滤
Omit<T, K>排除部分键Pick + Exclude
Record<K, V>键值映射对象索引签名
Exclude<T, U>从联合中剔除分发 + never
Extract<T, U>从联合中提取分发保留
NonNullable<T>去 null/undefinedT & {} 或分发剔除
ReturnType<T> / Parameters<T>函数返回/参数infer
Awaited<T>递归解包 Promiseinfer + 递归
Uppercase/Lowercase/Capitalize/Uncapitalize<S>字符串大小写模板字面量内置

映射类型(语法要点)

遍历键集合生成新对象类型,可增删修饰符(+/-)与重映射键:

ts
// 运行环境:tsc ≥ 4.5
// 基础映射:每个键套一层只读包装
type Boxed<T> = { [K in keyof T]: { value: T[K] } }
type A = Boxed<{ x: number; y: number }> // { x: { value: number }; y: { value: number } }

// 修饰符 +/-
type MyPartial<T> = { [K in keyof T]?: T[K] }
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
type MyRequired<T> = { [K in keyof T]-?: T[K] }

// as 重映射:转换键名(TS 4.1+),配合模板字面量做「事件类型」映射
type Events = { click: MouseEvent; input: string }
type HandlerMap = { [K in keyof Events as `on${Capitalize<K>}`]: (e: Events[K]) => void }
// → { onClick: (e: MouseEvent) => void; onInput: (e: string) => void }

// 按条件过滤键(去掉函数类型的属性)
type OnlyValueKeys<T> = { [K in keyof T as T[K] extends (...a: any[]) => any ? never : K]: T[K] }
type B = OnlyValueKeys<{ run: () => void; data: number }> // { data: number }

模板字面量类型(语法要点)

在字符串字面量上做类型级拼接,可用于协议/事件名/路径等强约束:

ts
// 运行环境:tsc ≥ 4.1
type Direction = 'up' | 'down' | 'left' | 'right'
type EventName = `on${Capitalize<Direction>}` // 'onUp' | 'onDown' | 'onLeft' | 'onRight'

type Route = `/user/${string}`      // 前缀校验
const good: Route = '/user/123'
// const bad: Route = '/admin/1'    // ❌ 必须以 /user/ 开头

// 解析:把一个字符串拆成结构化类型
type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : [S]
type Parts = Split<'a-b-c', '-'>    // ['a', 'b', 'c']

// 类型级 ParseUrl:把 '?id=1&page=2' 解析为 { id: '1'; page: '2' }

踩坑记录

  • 分发只在「裸类型参数」时发生:type E<T> = T extends ...;若 TT[]/Promise<T> 等包裹,则不会分发;
  • 映射类型默认保留原修饰符:readonly/? 需要显式 -readonly/-? 才能去掉;
  • 模板字面量里的 ${infer X} 一次只能推断一段,复杂解析需要递归 + 分步吃掉前缀;
  • 条件类型求值是惰性的(遇到未实例化的泛型会「悬而未决」),不要期望 IsString<T> 在泛型上下文立即化简;
  • 递归类型别太深:TS 有递归深度限制,可改用「尾递归 + 元组计数」技巧或拆分类型。

类型体操实战

类型体操 = 在类型层面「编程」,用于学习分发 / infer / 映射的组合。下面每题先给思路再给实现,全部可用 tsc --noEmit 验证(TS ≥ 4.5)。

Pick / Readonly(映射类型入门)

ts
// 运行环境:tsc ≥ 4.5
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }
type MyReadonly<T> = { readonly [P in keyof T]: T[P] }

interface Todo { title: string; desc: string; done: boolean }
type T1 = MyPick<Todo, 'title' | 'done'>        // { title: string; done: boolean }
type T2 = MyReadonly<Todo>                       // 全字段只读

TupleToObject / Length(元组与索引)

ts
// 运行环境:tsc ≥ 4.5
const tuple = ['tesla', 'model 3', 'model x'] as const
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P
}
type Car = TupleToObject<typeof tuple> // { tesla: 'tesla'; 'model 3': 'model 3'; ... }

type Length<T extends readonly unknown[]> = T['length']
type L = Length<typeof tuple> // 3(元素个数)

First / Last / Pop(数组递归)

ts
// 运行环境:tsc ≥ 4.5
type First<T extends unknown[]> = T extends [] ? never : T[0]
type Last<T extends unknown[]> = T extends [...infer _, infer Tail] ? Tail : never
type Pop<T extends unknown[]> = T extends [...infer Rest, unknown] ? Rest : never

type A = First<[1, 2, 3]>      // 1
type B = Last<[1, 2, 3]>       // 3
type C = Pop<[1, 2, 3]>        // [1, 2]

Exclude(分发 + never 的经典应用)

ts
// 运行环境:tsc ≥ 4.5
type MyExclude<T, U> = T extends U ? never : T
type D = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
// 推导:对 'a'|'b'|'c' 逐个判断,'a' 命中剔除,其余保留,never 自动从联合中消失

Awaited(infer + 递归)

ts
// 运行环境:tsc ≥ 4.5(TS ≥ 4.5 自带 Awaited,这里演示递归实现)
type MyAwaited<T> = T extends Promise<infer U>
  ? U extends Promise<unknown> ? MyAwaited<U> : U
  : T
type Nested = Promise<Promise<Promise<{ ok: true }>>>
type R = MyAwaited<Nested> // { ok: true }

DeepReadonly / DeepPartial(递归对象)

ts
// 运行环境:tsc ≥ 4.5
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
interface Cfg { server: { host: string; port: number } }
type RO = DeepReadonly<Cfg> // 所有层级 readonly
type OP = DeepPartial<Cfg>  // 所有层级可选

UnionToIntersection(逆变位点的分发技巧)

ts
// 运行环境:tsc ≥ 4.5 —— 函数参数位置的联合会「逆变」为交叉
type UnionToIntersection<U> =
  (U extends unknown ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never
type U = UnionToIntersection<{ a: 1 } | { b: 2 }> // { a: 1 } & { b: 2 }

建议练习顺序

  1. 手写 Partial / Required / Readonly / Pick / Omit / Record / Exclude / Extract / NonNullable
  2. 手写 ReturnType / Parameters / Awaited / InstanceType
  3. 尝试官方「Type Challenges」前 20 题(easy 组)检验以上实现;
  4. 用映射 + 模板字面量为一个真实项目写「事件名 → 处理器」类型。

声明文件与第三方类型治理

.d.ts 基础(语法要点)

.d.ts 只含类型、不含实现,是「给 JS 库描述类型」或「为全局变量/模块补类型」的文件。声明目标不同写法不同:

ts
// 运行环境:tsc ≥ 4.5(以下为声明片段,无运行产物)
// 1. 声明全局变量(脚本上下文,无 import/export)
declare const VERSION: string

// 2. 声明全局接口(挂在 window 上需配合 declare global)
interface Window {
  __APP_CONFIG__: { env: string }
}

// 3. 给没有类型的 npm 包写最小模块声明(ambient module)
//    文件:types/untyped-lib.d.ts
declare module 'untyped-lib' {
  export function start(opts?: { port?: number }): Promise<void>
  export const version: string
}
//    使用处直接 import:import { start } from 'untyped-lib'

// 4. 增强第三方模块类型(模块增强)
declare module 'vue' {
  interface ComponentCustomProperties {
    $api: (url: string) => Promise<unknown>
  }
}

三斜线指令用于显式声明文件间的依赖(多数情况下可以被 tsconfiginclude 替代):

ts
/// <reference types="node" />          // 引入 @types/node 的类型
/// <reference path="./other.d.ts" />   // 引入相邻声明文件

tsconfig 关键配置(工程实践)

配置作用建议
strict: true开启全部严格检查(noImplicitAny 等 7 项)新项目必开
noUncheckedIndexedAccess索引访问结果含 undefined排隐患,推荐开
moduleResolution: "bundler"现代打包器解析规则(Node16/node 二选一)跟随 module
exactOptionalPropertyTypes可选属性不接受显式 undefined更精确,可开
skipLibCheck: true跳过 .d.ts 类型检查(加速、容忍坏声明)通常开启
paths / baseUrl路径别名与打包器/tsconfig-paths 保持一致
types: []仅加载显式列出的 @types限制全局注入
noEmit / emitDeclarationOnly只做类型检查 / 只出声明CI 与库工程
jsonc
// 运行环境:tsc ≥ 5.0 —— 现代应用推荐基线(节选)
{
  "compilerOptions": {
    "strict": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true,
    "paths": { "@/*": ["./src/*"] }
  }
}

第三方类型治理(实战)

按优先级治理 any 与缺失类型:

  1. type-only 导入import type { X } from 'y' 只进类型不进运行时,配合 isolatedModules 安全;
  2. @types/* 补齐:大部分流行库有 @types/lib,随包使用或单独安装;检查包内是否自带类型(types/exports 字段);
  3. 缺失类型的库:先用 declare module 'lib' 白名单声明用到的 API(见上),从「最小可用」逐步补全;避免整包 any
  4. window/全局扩展:通过 declare global + 接口增强,收敛全局变量类型;
  5. 升级逃逸:确需宽松处用 unknown + 收窄函数(isUser 式守卫),而非裸 any;逃生门收敛在单个文件内;
  6. 从 JS 迁移:开 allowJs + checkJs,用 // @ts-check 与 JSDoc @param/@type 渐进标注,再逐步转 .ts

治理前后对比示例:

ts
// 运行环境:tsc ≥ 5.0
// ✗ 差:any 泛滥,错误在运行时才暴露
function load(cfg: any) {
  return fetch(cfg.url).then((r: any) => r.data) // r.data 是什么?全靠猜
}

// ✓ 好:unknown 输入 + 守卫收窄 + 显式返回类型
interface Payload { data: { id: number } }
function loadSafe(cfg: { url: string }): Promise<number> {
  return fetch(cfg.url)
    .then((r) => r.json() as unknown)
    .then((json) => {
      if (!isPayload(json)) throw new Error('响应结构异常')
      return json.data.id
    })
}
function isPayload(v: unknown): v is Payload {
  return !!v && typeof v === 'object' && 'data' in v
}

踩坑记录

  • 忘装 @types/node 却引用 process → 「Cannot find name 'process'」;全局类型依赖要写进 devDependencies 而非运行时依赖;
  • import type 与值导入混用时的隔离问题(isolatedModules 下跨文件类型引用必须 import type);
  • 第三方包类型与实现版本不匹配(多为 types 滞后的包):锁定 @types 大版本或用 patch-package 修补;
  • .d.ts 放进 src 且被 include 遗漏的路径别名:用 tsconfiginclude: ["src", "types"] 覆盖;
  • 声明文件里出现实现语句会报错:.d.ts 只能有 declare/type/interface/导出签名,不能有赋值与函数体。

状态与参考

  • 状态:已收录(2026-09-02 完成领域概览规划的 4 项主题)。
  • 运行环境:类型示例需 tsc --noEmit 验证(TS ≥ 4.5,推荐 5.x);涉及 fetch/DOM 的示例在浏览器 + DOM lib 下运行。
  • 参考TypeScript 官方文档Type Challengestsconfig 参考

下一步

  • [ ] 把「类型体操实战」章节与 Type Challenges 的 easy/medium 题逐步对照,补注释
  • [ ] 沉淀项目里真实遇到的第三方类型治理案例(升级依赖、绕开坏声明等)到踩坑记录
  • [ ] 结合前端 Vue 3 类型章节补一份「框架内类型实践」小节

写作规范与页面规划请参阅领域概览

基于 VitePress 构建 · 内容以知识共享方式沉淀