TypeScript
已收录:本页完成领域概览规划清单中 TypeScript 的 4 项内容(类型层级与类型收窄、条件/映射/模板字面量类型、类型体操实战、声明文件与第三方类型治理)。示例标注运行环境,可直接复制验证。
本页按「语法要点 → 运行机制 → 示例 → 踩坑」组织,覆盖类型系统与工程实践:
类型层级与类型收窄
类型层级(语法要点)
TypeScript 的类型可以看作集合:一个值满足的类型越多,该类型越「宽」。层级从上到下:
unknown ← 顶层类型(任何值都兼容)
├── any ← 逃逸口(可赋给任何类型,也可收任何类型)
├── string | number ← 联合类型(并集)
│ ├── string
│ └── number
├── ...
└── never ← 底层类型(空集,不可能有值)核心判断:类型 A 能赋值给类型 B ⇔ A 是 B 的子类型(A ⊆ B)。unknown 只能收不能给(除 any);never 能给任何类型但不能被任何类型赋值;any 双向兼容,是类型检查的「逃生舱」。
// 运行环境: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] | 键集合 / 索引访问 | 元编程基础 |
类型收窄(语法要点 + 运行机制)
联合类型在运行时只有一种形态,编译器需要借助控制流分析把宽类型「收窄」成窄类型才能安全使用:
// 运行环境: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
}
}其他收窄手段:
| 手段 | 示例 | 说明 |
|---|---|---|
typeof | typeof x === 'string' | 原始类型;注意 typeof null === 'object' |
instanceof | x instanceof Date | 类/构造实例 |
in | 'kind' in s | 判断对象是否含某键 |
| truthiness | if (x != null) | 排除 null/undefined 与假值 |
| 相等收窄 | x === 'ok' | 与字面量比较 |
| 可辨识联合 | s.kind === 'circle' | 最常用,见上 |
| 类型谓词 | isX(x): x is X | 自定义收窄(见下) |
| 断言函数 | assertX(x): asserts x is X | 不通过即抛错,之后窄类型生效 |
Array.isArray | Array.isArray(x) | 数组收窄 |
自定义守卫函数(type predicate),写校验逻辑后把结果「翻译」给类型系统:
// 运行环境: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 枚举」的推荐做法:
// 运行环境: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 != null或v && 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 是裸类型参数且为联合类型时,会对联合的每个成员分别判断再合并结果——这是很多工具类型的基石。
// 运行环境: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 从结构里「抽取」一个待推断类型,通常与条件类型搭配:
// 运行环境: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/undefined | T & {} 或分发剔除 |
ReturnType<T> / Parameters<T> | 函数返回/参数 | infer |
Awaited<T> | 递归解包 Promise | infer + 递归 |
Uppercase/Lowercase/Capitalize/Uncapitalize<S> | 字符串大小写 | 模板字面量内置 |
映射类型(语法要点)
遍历键集合生成新对象类型,可增删修饰符(+/-)与重映射键:
// 运行环境: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 }模板字面量类型(语法要点)
在字符串字面量上做类型级拼接,可用于协议/事件名/路径等强约束:
// 运行环境: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 ...;若T被T[]/Promise<T>等包裹,则不会分发; - 映射类型默认保留原修饰符:
readonly/?需要显式-readonly/-?才能去掉; - 模板字面量里的
${infer X}一次只能推断一段,复杂解析需要递归 + 分步吃掉前缀; - 条件类型求值是惰性的(遇到未实例化的泛型会「悬而未决」),不要期望
IsString<T>在泛型上下文立即化简; - 递归类型别太深:TS 有递归深度限制,可改用「尾递归 + 元组计数」技巧或拆分类型。
类型体操实战
类型体操 = 在类型层面「编程」,用于学习分发 / infer / 映射的组合。下面每题先给思路再给实现,全部可用
tsc --noEmit验证(TS ≥ 4.5)。
Pick / Readonly(映射类型入门)
// 运行环境: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(元组与索引)
// 运行环境: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(数组递归)
// 运行环境: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 的经典应用)
// 运行环境: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 + 递归)
// 运行环境: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(递归对象)
// 运行环境: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(逆变位点的分发技巧)
// 运行环境: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 }建议练习顺序
- 手写
Partial / Required / Readonly / Pick / Omit / Record / Exclude / Extract / NonNullable; - 手写
ReturnType / Parameters / Awaited / InstanceType; - 尝试官方「Type Challenges」前 20 题(easy 组)检验以上实现;
- 用映射 + 模板字面量为一个真实项目写「事件名 → 处理器」类型。
声明文件与第三方类型治理
.d.ts 基础(语法要点)
.d.ts 只含类型、不含实现,是「给 JS 库描述类型」或「为全局变量/模块补类型」的文件。声明目标不同写法不同:
// 运行环境: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>
}
}三斜线指令用于显式声明文件间的依赖(多数情况下可以被 tsconfig 的 include 替代):
/// <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 与库工程 |
// 运行环境: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 与缺失类型:
- type-only 导入:
import type { X } from 'y'只进类型不进运行时,配合isolatedModules安全; @types/*补齐:大部分流行库有@types/lib,随包使用或单独安装;检查包内是否自带类型(types/exports字段);- 缺失类型的库:先用
declare module 'lib'白名单声明用到的 API(见上),从「最小可用」逐步补全;避免整包any; window/全局扩展:通过declare global+ 接口增强,收敛全局变量类型;- 升级逃逸:确需宽松处用
unknown+ 收窄函数(isUser式守卫),而非裸any;逃生门收敛在单个文件内; - 从 JS 迁移:开
allowJs+checkJs,用// @ts-check与 JSDoc@param/@type渐进标注,再逐步转.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遗漏的路径别名:用tsconfig的include: ["src", "types"]覆盖; - 声明文件里出现实现语句会报错:
.d.ts只能有declare/type/interface/导出签名,不能有赋值与函数体。
状态与参考
- 状态:已收录(2026-09-02 完成领域概览规划的 4 项主题)。
- 运行环境:类型示例需
tsc --noEmit验证(TS ≥ 4.5,推荐 5.x);涉及fetch/DOM的示例在浏览器 + DOM lib 下运行。 - 参考:TypeScript 官方文档、Type Challenges、tsconfig 参考。
下一步
- [ ] 把「类型体操实战」章节与 Type Challenges 的 easy/medium 题逐步对照,补注释
- [ ] 沉淀项目里真实遇到的第三方类型治理案例(升级依赖、绕开坏声明等)到踩坑记录
- [ ] 结合前端 Vue 3 类型章节补一份「框架内类型实践」小节
写作规范与页面规划请参阅领域概览。