TypeScript 类型系统:从结构类型到类型编程 / The TypeScript Type System from Structural Typing to Type-Level Programming
📅 创建时间:2026-07-28 🏷️ 标签:#TypeScript #TypeSystem #泛型 #类型编程 📚 前置知识:[[01-javascript-runtime-and-language]]
📋 本章目标
- 理解 TypeScript 结构类型系统的设计哲学,以及与名义类型的本质区别
- 掌握基本类型、字面量类型、
const推断和类型收窄的完整工具箱 - 理解
unknown/any/never的语义差异和选择决策树 - 能够区分
type和interface的使用场景,包括声明合并 - 掌握泛型约束、默认值、"泛型只用一次"启发式与实战模式
- 理解
keyof、typeof、索引访问、映射类型、条件类型和infer构成类型编程体系 - 能够使用
as const、satisfies、模板字面量类型和工具类型表达精确约束
第1部分:结构类型 —— TypeScript 的哲学基石
1.1 什么是结构类型
TypeScript 采用结构类型系统(Structural Type System):类型兼容性由值的形状决定,而非声明的名称或继承链。
┌─────────────────────────────────────────────────────────────┐
│ 结构类型 vs 名义类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 名义类型(Nominal)—— Java / C# │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ class Dog { bark() {} } │ │
│ │ class Cat { meow() {} } │ │
│ │ let d: Dog = new Cat() // ❌ 类型不兼容 │ │
│ │ // 即使结构完全相同,名称不同就不可赋值 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 结构类型(Structural)—— TypeScript │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ interface Dog { name: string; bark(): void } │ │
│ │ interface Cat { name: string; bark(): void } │ │
│ │ let d: Dog = { name: "x", bark(){} } as Cat // ✅ │ │
│ │ // 形状兼容即可赋值,名称无关 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘这个设计源于 JavaScript 生态的现实:JSON 数据、第三方库对象、网络响应都没有类型标签。TypeScript 必须接受"任何形状匹配的对象"而不是"来自特定类的实例"。
1.2 鸭子类型与结构类型的区别
┌─────────────────────────────────────────────────────────────┐
│ 鸭子类型 → 结构类型的演进 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 鸭子类型(运行时): │
│ "走起来像鸭子,叫起来像鸭子,那就是鸭子" │
│ → 运行时检查属性存在性,没有类型声明 │
│ │
│ 结构类型(编译时): │
│ "编译器在编译时检查形状是否兼容" │
│ → 类型安全 + 鸭子类型的灵活性 │
│ │
│ TypeScript = 结构类型 = 编译时的鸭子类型 │
│ │
└─────────────────────────────────────────────────────────────┘结构类型的实际含义:函数参数只需要声明它真正用到的字段,而非完整的对象类型。
// 不要这样写——过度约束
function printUser(user: { id: string; name: string; email: string;
avatar: string; createdAt: Date; updatedAt: Date }) {
console.log(user.name)
}
// 这样写——最小约束,最大兼容
function printUser(user: { name: string }) {
console.log(user.name)
}第2部分:基础类型、字面量类型与推断
2.1 基本类型与 let / const 推断
TypeScript 对 let 和 const 采用不同的推断策略:
┌─────────────────────────────────────────────────────────────┐
│ let vs const 推断策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ let x = "hello" → x: string (宽类型,可重新赋值) │
│ const x = "hello" → x: "hello" (字面量类型,不可变) │
│ │
│ let n = 42 → n: number (宽类型) │
│ const n = 42 → n: 42 (字面量类型) │
│ │
│ let arr = [1,2,3] → arr: number[] │
│ const arr = [1,2,3] → arr: number[] (注意:元素仍可改) │
│ │
└─────────────────────────────────────────────────────────────┘// const 声明的对象:属性类型被拓宽,但引用不可变
const config = {
host: "localhost", // string(不是 "localhost")
port: 8080, // number(不是 8080)
}
// 需要字面量类型时,使用 as const
const config2 = {
host: "localhost",
port: 8080,
} as const
// config2: { readonly host: "localhost"; readonly port: 8080 }2.2 数组、元组与对象类型
// 数组:同类型元素
const names: string[] = ["Ada", "Grace"]
// 元组:固定长度、各位置独立类型
type Pair = [string, number]
const entry: Pair = ["age", 30]
// 可变元组(rest elements)
type CSVLine = [string, ...number[]]
const row: CSVLine = ["2026-07", 100, 200, 300]
// 对象:显式声明优于隐式推断(在函数边界)
interface User {
id: string
name: string
email?: string // 可选属性
readonly createdAt: Date // 只读属性
}2.3 优先让编译器推断局部变量
函数参数、公共返回值、模块边界应显式书写类型。局部变量优先推断:
// ✅ 让编译器推断局部变量
const retries = 3 // number
const names = ["Ada"] // string[]
// ✅ 函数边界显式声明
function findUser(id: string): Promise<User | undefined> {
// 内部逻辑可以推断
const query = `SELECT * FROM users WHERE id = ${id}`
return db.query(query)
}第3部分:联合类型、判别联合与穷尽检查
3.1 联合类型的基本概念
联合类型表示"多个类型中的某一个",是 TypeScript 最强大的特性之一。
┌─────────────────────────────────────────────────────────────┐
│ 联合类型的本质 │
├─────────────────────────────────────────────────────────────┤
│ │
│ type A = string | number │
│ │
│ ┌─────────┐ │
│ │ string │──────┐ │
│ └─────────┘ │ ┌───────────────────┐ │
│ ├───→│ string | number │ │
│ ┌─────────┐ │ └───────────────────┘ │
│ │ number │──────┘ │
│ └─────────┘ │
│ │
│ 未收窄前,只能访问各成员的共有属性 │
│ 收窄后,可访问该分支的全部属性 │
│ │
└─────────────────────────────────────────────────────────────┘3.2 判别联合(Discriminated Unions)
判别联合是 TypeScript 中最优雅的状态建模方式:用一个字面量字段区分变体。
// 用 status 字段作为判别属性
type RequestState<T> =
| { status: "idle" }
| { status: "loading"; progress?: number }
| { status: "success"; data: T }
| { status: "error"; message: string; code?: number }
function render<T>(state: RequestState<T>): string {
switch (state.status) {
case "idle":
return "等待中"
case "loading":
// state.progress 可访问
return `加载中... ${state.progress ?? 0}%`
case "success":
// state.data 可访问
return `成功:${JSON.stringify(state.data)}`
case "error":
// state.message 和 state.code 可访问
return `错误 [${state.code ?? "未知"}]: ${state.message}`
}
}┌─────────────────────────────────────────────────────────────┐
│ 判别联合的设计要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 每个变体共享一个判别字段(discriminant) │
│ → 必须是字面量类型:"idle" | "loading" | ... │
│ │
│ 2. 判别字段的值在整个联合类型中互不相交 │
│ → 不能有两个变体都有 status: "loading" │
│ │
│ 3. switch 语句收窄到各分支后,TS 自动推断其他字段 │
│ → 无需手动类型断言 │
│ │
└─────────────────────────────────────────────────────────────┘3.3 用 never 实现穷尽检查
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`)
}
function render<T>(state: RequestState<T>): string {
switch (state.status) {
case "idle": return "等待中"
case "loading": return "加载中..."
case "success": return "成功"
case "error": return "错误"
default:
// 如果后续有人加了新变体但没更新 switch,这里会编译报错
return assertNever(state)
}
}assertNever 利用了 never 类型:如果把非 never 的值传给参数类型为 never 的函数,TypeScript 会报错。这是一种编译时的安全网。
第4部分:unknown / any / never 三剑客
4.1 三种顶层/底层类型
┌─────────────────────────────────────────────────────────────┐
│ TypeScript 类型层级(简化) │
├─────────────────────────────────────────────────────────────┤
│ │
│ unknown │
│ ┌─────┐ │
│ │ any │ ← 不在正常层级中,是"逃逸" │
│ └─────┘ │
│ │ │
│ ┌──────────┼──────────┐ │
│ │ │ │ │
│ string number object │
│ │ │ │ │
│ └──────────┼──────────┘ │
│ │ │
│ string | number │
│ (联合类型) │
│ │ │
│ never │
│ ┌─────┐ │
│ │never│ ← 底层类型,是所有人的子类型 │
│ └─────┘ │
│ │
│ unknown = 顶层类型(所有类型都可赋值给它) │
│ never = 底层类型(它可以赋值给任何类型) │
│ any = 既像顶层又像底层(关闭类型检查) │
│ │
└─────────────────────────────────────────────────────────────┘4.2 选择决策树
┌─────────────────────────────────────────────────────────────┐
│ 选择哪个类型的决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 这个值的类型我: │
│ │
│ 完全确定 → 使用具体类型 │
│ 例:function add(a: number, b: number): number │
│ │
│ 不确定但需要先接收下来 → 使用 unknown │
│ 例:function parse(input: unknown): Result │
│ → 然后收窄(typeof / instanceof / schema 验证) │
│ │
│ 理论上不可达 → 使用 never │
│ 例:抛出异常、死循环、穷尽检查 │
│ │
│ any → 尽量不要用 │
│ → 迁移遗留代码时的临时方案 │
│ → 泛型约束无法满足时,unknown 几乎总比 any 更好 │
│ │
└─────────────────────────────────────────────────────────────┘4.3 unknown 的收窄模式
function processValue(value: unknown): string {
// 方式1:typeof 收窄
if (typeof value === "string") {
return value.toUpperCase() // value: string
}
// 方式2:instanceof 收窄
if (value instanceof Date) {
return value.toISOString() // value: Date
}
// 方式3:自定义类型守卫
if (isUser(value)) {
return value.name // value: User
}
// 方式4:in 操作符
if (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string"
) {
return (value as { name: string }).name
}
throw new Error("Unsupported value type")
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
)
}第5部分:type vs interface —— 选型的艺术
5.1 核心差异
┌─────────────────────────────────────────────────────────────┐
│ type vs interface 能力对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 能力 type interface │
│ ─────────────────────────────────────────────────────── │
│ 描述对象形状 ✅ ✅ │
│ 描述联合类型 ✅ ❌ │
│ 描述元组 ✅ ✅(受限) │
│ 声明合并 ❌ ✅ │
│ extends / 交叉 ✅ (&) ✅ (extends) │
│ 映射类型 ✅ ❌ │
│ 条件类型 ✅ ❌ │
│ 工具类型 ✅ ❌ │
│ 错误信息可读性 一般 ✅(更好) │
│ │
└─────────────────────────────────────────────────────────────┘5.2 声明合并(Declaration Merging)
这是 interface 独有的能力,适用于需要扩展第三方类型或分模块声明接口的场景:
// 分两次声明同一个 interface,TS 会自动合并
interface Window {
title: string
}
interface Window {
ts: typeof import("typescript")
}
// 结果等价于:
// interface Window {
// title: string
// ts: typeof import("typescript")
// }type 重复声明会报错。声明合并是 interface 最关键的差异化能力。
5.3 何时用哪个
┌─────────────────────────────────────────────────────────────┐
│ 选型建议 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 优先使用 interface,当: │
│ • 定义对象的形状(尤其是公共 API、组件 props) │
│ • 需要声明合并(扩展第三方库、模块增强) │
│ • 希望获得更好的错误提示(interface 展开后错误更清晰) │
│ │
│ 必须使用 type,当: │
│ • 需要联合类型、交叉类型 │
│ • 需要映射类型、条件类型 │
│ • 需要元组(虽然 interface 也可以,但 type 更直观) │
│ • 需要工具类型(Partial、Pick 等基于 type 的操作) │
│ │
│ 通用原则: │
│ → 定义对象形状用 interface,类型运算用 type │
│ → 同一个项目中保持一致 │
│ │
└─────────────────────────────────────────────────────────────┘// ✅ 对象形状 → interface
interface User {
id: string
name: string
email: string
}
// ✅ 类型运算 → type
type UserSummary = Pick<User, "id" | "name">
type UserOrError = User | { error: string }
// ✅ 函数的形状也可以用 type(更紧凑)
type GetUser = (id: string) => Promise<User>
// 但 interface 也可以表示函数
interface GetUserFn {
(id: string): Promise<User>
}第6部分:泛型 —— 表达关系而非减少字符
6.1 泛型的本质
泛型的价值不是写更少的代码,而是表达输入和输出之间的类型关系。
┌─────────────────────────────────────────────────────────────┐
│ 泛型表达的是"关系" │
├─────────────────────────────────────────────────────────────┤
│ │
│ 不用泛型: │
│ function first(arr: any[]): any { return arr[0] } │
│ → 输入和输出之间没有类型关联 │
│ │
│ 用泛型: │
│ function first<T>(arr: T[]): T | undefined { │
│ return arr[0] │
│ } │
│ → "输出类型 = 数组元素类型" 这个关系被精确表达 │
│ │
└─────────────────────────────────────────────────────────────┘6.2 泛型约束
约束应尽可能小 —— 只声明泛型"真正需要"的能力。
// ❌ 过度约束
function sortBy<T extends { id: string; name: string; createdAt: Date }>(
items: T[]
): T[] { /* ... */ }
// ✅ 最小约束 —— 只声明真正需要的
function byId<T extends { id: string }>(items: readonly T[]) {
return new Map(items.map(item => [item.id, item]))
}
// ✅ 不需要任何约束时,不写 extends
function identity<T>(value: T): T {
return value
}6.3 泛型默认值
// 为泛型参数提供默认类型
interface ApiResponse<T = unknown> {
success: boolean
data: T
message: string
}
// 使用默认值:T 推定为 unknown
const r1: ApiResponse = { success: true, data: null, message: "ok" }
// 显式指定类型
const r2: ApiResponse<User> = {
success: true,
data: { id: "1", name: "Ada" },
message: "ok",
}6.4 "泛型只用一次"启发式
┌─────────────────────────────────────────────────────────────┐
│ "泛型只用一次" 检查清单 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 如果泛型参数 T 在函数签名中只出现一次: │
│ │
│ ❌ function foo<T extends string>(x: T): void │
│ → T 只出现在参数,与其他参数/返回值无关联 │
│ → 泛型没有表达任何关系 │
│ → 直接用 string 即可 │
│ │
│ ✅ function first<T>(arr: T[]): T | undefined │
│ → T 出现在参数和返回值,表达了关系 │
│ │
│ ⚠️ 例外:函数重载、高阶函数、类型构建器 │
│ function identity<T>(x: T): T ← T 出现两次,成立 │
│ │
└─────────────────────────────────────────────────────────────┘6.5 实战泛型模式
// 模式1:约束链 —— 一个泛型的约束依赖另一个
function mergeConfig<Base, Override extends Partial<Base>>(
base: Base,
override: Override
): Base & Override {
return { ...base, ...override }
}
// 模式2:高阶泛型 —— 泛型参数本身是泛型
type WithId<T> = T & { id: string }
type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date }
type FullEntity<T> = WithTimestamps<WithId<T>>
// FullEntity<{ name: string }> =
// { name: string; id: string; createdAt: Date; updatedAt: Date }
// 模式3:工厂函数返回泛型
function createStore<T>(initial: T) {
let state = initial
return {
get: (): T => state,
set: (next: T): void => { state = next },
}
}第7部分:类型编程基础 —— keyof、typeof 与索引访问
7.1 keyof —— 获取键的联合类型
┌─────────────────────────────────────────────────────────────┐
│ keyof 运算符 │
├─────────────────────────────────────────────────────────────┤
│ │
│ interface User { │
│ id: string │
│ name: string │
│ age: number │
│ } │
│ │
│ type UserKey = keyof User │
│ // "id" | "name" | "age" │
│ │
│ keyof 任何对象类型 → 其所有键的字面量联合 │
│ │
└─────────────────────────────────────────────────────────────┘// keyof 的典型应用:类型安全的属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user: User = { id: "1", name: "Ada", age: 30 }
const name = getProperty(user, "name") // name: string
// getProperty(user, "email") // ❌ 编译错误7.2 typeof —— 从值获取类型
TypeScript 的 typeof 有两层含义:JavaScript 运行时的 typeof 和 TypeScript 类型层面的 typeof。
// JS 运行时 typeof:返回字符串
console.log(typeof "hello") // "string"
// TS 类型层面 typeof:从值提取类型
const config = {
host: "localhost",
port: 8080,
retry: 3,
}
type Config = typeof config
// { host: string; port: number; retry: number }
// 典型用法:从已有值推导相关类型
function makeStore<T>(initial: T) {
return { get: () => initial, set: (v: T) => { /* ... */ } }
}
const userStore = makeStore({ id: "1", name: "Ada" })
type UserStore = typeof userStore
// { get: () => { id: string; name: string }; set: (v: { id: string; name: string }) => void }7.3 索引访问类型
// T[K] —— 获取 T 中键 K 对应的值类型
interface User {
id: string
name: string
tags: string[]
profile: {
avatar: string
bio: string
}
}
type UserId = User["id"] // string
type UserTags = User["tags"] // string[]
type UserProfile = User["profile"] // { avatar: string; bio: string }
// 联合键获取联合值类型
type StringFields = User["id" | "name"] // string
// 嵌套索引
type AvatarType = User["profile"]["avatar"] // string
// 与 keyof 组合——获取所有值类型的联合
type UserValues = User[keyof User]
// string | string[] | { avatar: string; bio: string }7.4 typeof + keyof + 索引访问的组合
const routes = {
home: "/",
about: "/about",
user: "/user/:id",
} as const
// 获取所有路由路径的类型
type RoutePath = (typeof routes)[keyof typeof routes]
// "/" | "/about" | "/user/:id"
// 这是从运行时常量推导类型的最强模式第8部分:映射类型 —— 转换对象形状
8.1 基本映射类型
┌─────────────────────────────────────────────────────────────┐
│ 映射类型的语法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ { [K in 联合类型]: 值类型 } │
│ │
│ K 遍历联合类型中的每个成员,为每个 K 生成一个属性 │
│ │
│ 输入:{ id: string; name: string; age: number } │
│ │
│ type Nullable<T> = { [K in keyof T]: T[K] | null } │
│ │
│ 输出:{ id: string | null; name: string | null; │
│ age: number | null } │
│ │
└─────────────────────────────────────────────────────────────┘// 基础映射:每个属性变成只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
// 每个属性变成可选
type MyPartial<T> = {
[K in keyof T]?: T[K]
}8.2 修饰符:-readonly 和 -?
// 去除只读
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
// 去除可选
type Required<T> = {
[K in keyof T]-?: T[K]
}
// 组合:同时去除 readonly 和 ?
type MutableAndRequired<T> = {
-readonly [K in keyof T]-?: T[K]
}8.3 键重映射(Key Remapping via as)
TypeScript 4.1 引入了 as 子句,可以在映射过程中重命名键。
┌─────────────────────────────────────────────────────────────┐
│ 键重映射的三种经典模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 模式1:添加前缀/后缀 │
│ type WithPrefix<T> = { │
│ [K in keyof T as `on${Capitalize<string & K>}`]: T[K] │
│ } │
│ // { name: string } → { onName: string } │
│ │
│ 模式2:过滤键 │
│ type OnlyStrings<T> = { │
│ [K in keyof T as T[K] extends string ? K : never] │
│ : T[K] │
│ } │
│ // 只保留值为 string 类型的属性 │
│ │
│ 模式3:基于值类型变换键 │
│ type EventHandlers<T> = { │
│ [K in keyof T as T[K] extends (...args: any[]) => any │
│ ? K : never]: T[K] │
│ } │
│ // 只保留函数类型的属性 │
│ │
└─────────────────────────────────────────────────────────────┘// 实际例子:Getter 类型
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
}
type PersonGetters = Getters<Person>
// {
// getName: () => string
// getAge: () => number
// }第9部分:条件类型与 infer
9.1 基本条件类型
条件类型的语法类似三元表达式,但运行在类型层面:
┌─────────────────────────────────────────────────────────────┐
│ 条件类型语法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ T extends U ? X : Y │
│ │
│ 如果 T 是 U 的子类型 → 结果为 X │
│ 否则 → 结果为 Y │
│ │
│ 示例: │
│ type IsString<T> = T extends string ? true : false │
│ IsString<"hello"> → true │
│ IsString<42> → false │
│ │
└─────────────────────────────────────────────────────────────┘9.2 分配条件类型(Distributive Conditional Types)
当条件类型的检查对象是裸泛型参数且传入联合类型时,条件类型会自动分配到每个成员。
type ToArray<T> = T extends any ? T[] : never
// 传入联合类型 → 分配
type Result = ToArray<string | number>
// 等价于:ToArray<string> | ToArray<number>
// 结果:string[] | number[]
// 阻止分配:用方括号包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never
type Result2 = ToArrayNoDistribute<string | number>
// 结果:(string | number)[]9.3 infer —— 在条件类型中提取类型
infer 是类型编程中最强大的工具之一:在 extends 子句中声明一个待推断的类型变量。
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never
type E1 = ElementType<string[]> // string
type E2 = ElementType<number> // never
// 提取函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = MyReturnType<() => string> // string
// 提取 Promise 内部类型
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T
type A1 = MyAwaited<Promise<Promise<number>>> // number
// 提取函数第一个参数类型
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any
? F : never
type P1 = FirstParam<(a: string, b: number) => void> // string┌─────────────────────────────────────────────────────────────┐
│ infer 的常见使用场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ T extends Array<infer U> → 提取数组元素 │
│ T extends Promise<infer U> → 提取 Promise 值 │
│ T extends (...args: any[]) → 提取函数返回类型 │
│ => infer R │
│ T extends (...args: infer P) → 提取函数参数元组 │
│ => any │
│ T extends { data: infer D } → 提取对象属性 │
│ T extends `${infer A}.${infer B}` → 提取模板字面量部分 │
│ │
└─────────────────────────────────────────────────────────────┘第10部分:模板字面量类型
10.1 基本语法
TypeScript 4.1 引入了模板字面量类型,将字符串模式匹配带入了类型系统。
type World = "world"
type Greeting = `hello ${World}` // "hello world"
// 联合类型在模板中自动分配(笛卡尔积)
type Color = "red" | "blue"
type Size = "sm" | "lg"
type ButtonVariant = `${Color}-${Size}`
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"10.2 模式匹配与提取
// 提取字符串的特定部分
type GetLastName<T extends string> =
T extends `${infer First} ${infer Last}` ? Last : never
type L = GetLastName<"Ada Lovelace"> // "Lovelace"
// 提取文件扩展名
type FileExtension<T extends string> =
T extends `${string}.${infer Ext}` ? Ext : never
type Ext = FileExtension<"app.config.ts"> // "ts"
// 递归提取路径段
type SplitPath<T extends string> =
T extends `${infer Seg}/${infer Rest}`
? Seg | SplitPath<Rest>
: T
type Segments = SplitPath<"a/b/c"> // "a" | "b" | "c"10.3 与映射类型组合
// 为对象的每个属性创建 setter 函数类型
type Setters<T> = {
[K in keyof T & string as `set${Capitalize<K>}`]: (value: T[K]) => void
}
interface Config {
theme: "light" | "dark"
fontSize: number
}
type ConfigSetters = Setters<Config>
// {
// setTheme: (value: "light" | "dark") => void
// setFontSize: (value: number) => void
// }10.4 内置字符串操作类型
// TypeScript 内置了四个字符串操作类型
type A = Uppercase<"hello"> // "HELLO"
type B = Lowercase<"HELLO"> // "hello"
type C = Capitalize<"hello"> // "Hello"
type D = Uncapitalize<"Hello"> // "hello"第11部分:类型收窄的全套工具箱
11.1 收窄策略总览
┌─────────────────────────────────────────────────────────────┐
│ TypeScript 类型收窄工具箱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 策略 适用场景 收窄依据 │
│ ────────────────────────────────────────────────────── │
│ typeof val 原始类型 JS typeof 结果 │
│ instanceof Cls 类实例 原型链 │
│ "key" in obj 对象属性存在性 hasOwn + proto │
│ val === literal 精确值比较 严格相等 │
│ val != null null/undefined 排除 不等比较 │
│ if (val) 真值检查 强制布尔转换 │
│ Array.isArray(val) 数组检查 Array.isArray │
│ discriminated union 状态建模 switch + 判别 │
│ type predicate 自定义类型守卫 返回值是 x is T │
│ assertion function 断言函数 返回值是 asserts │
│ │
└─────────────────────────────────────────────────────────────┘11.2 类型谓词(Type Predicates)
类型谓词 x is T 是自定义类型守卫的返回值类型。它告诉 TypeScript:如果这个函数返回 true,那么参数就是类型 T。
interface Cat {
meow(): void
}
interface Dog {
bark(): void
}
// 类型谓词:返回类型是 value is Cat
function isCat(value: unknown): value is Cat {
return (
typeof value === "object" &&
value !== null &&
"meow" in value &&
typeof (value as Cat).meow === "function"
)
}
function handleAnimal(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow() // animal: Cat
} else {
animal.bark() // animal: Dog
}
}┌─────────────────────────────────────────────────────────────┐
│ 类型谓词的使用要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 谓词函数内部实现必须正确——TS 不验证实现的正确性 │
│ → 写得不对 = 运行时类型错误 + 编译器不报错 │
│ → 谓词是"对编译器的承诺",责任在开发者 │
│ │
│ 2. 不要过度使用类型谓词来绕过类型检查 │
│ → 能用 typeof / instanceof 解决的优先用 │
│ → 复杂的数据验证应该在运行时用 schema 库 │
│ │
│ 3. 类型谓词可以组合 │
│ function isStringOrNumber(x: unknown) │
│ : x is string | number { ... } │
│ │
└─────────────────────────────────────────────────────────────┘11.3 断言函数(Assertion Functions)
TypeScript 3.7 引入了断言函数,使用 asserts x is T 语法。与类型谓词不同,断言函数不返回布尔值,而是通过抛出异常来表示"不满足条件"。
// 断言函数:如果 value 不是 string,抛出异常
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`Expected string, got ${typeof value}`)
}
}
declare const input: unknown
assertString(input)
// 从这里开始,input 的类型是 string
input.toUpperCase() // ✅
// 另一种形式:asserts condition
function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? "Assertion failed")
}
}
declare const x: string | undefined
assert(x !== undefined)
// 从这里开始,x 的类型是 string
x.toUpperCase() // ✅第12部分:as const、satisfies 与常量类型参数
12.1 as const —— 深度只读字面量推断
as const 做三件事:将属性标记为 readonly,将字面量类型收窄到精确值,将数组变为只读元组。
// 不加 as const:类型被拓宽
const routes1 = { home: "/", user: "/user" }
// { home: string; user: string }
// 加 as const:类型精确收窄
const routes2 = { home: "/", user: "/user" } as const
// { readonly home: "/"; readonly user: "/user" }
// 数组的区别
const arr1 = ["a", "b", "c"]
// string[]
const arr2 = ["a", "b", "c"] as const
// readonly ["a", "b", "c"]
// 典型场景:从常量对象推导精确联合类型
const colors = ["red", "green", "blue"] as const
type Color = (typeof colors)[number] // "red" | "green" | "blue"12.2 satisfies 运算符(TS 4.9+)
satisfies 检查类型兼容性但不改变表达式的推断类型。这是"验证而不拓宽"的最优雅方式。
// 场景:希望 colors 满足 string[],但保留字面量类型
const colors = ["red", "green", "blue"] satisfies string[]
// colors 的类型仍是 readonly ["red", "green", "blue"]
// 但 TypeScript 验证了它满足 string[] 约束
type ColorName = (typeof colors)[number] // "red" | "green" | "blue"
// 对比:不加 satisfies
const colors2 = ["red", "green", "blue"]
// colors2: string[]
// 字面量类型丢失!
// 对比:加类型注解
const colors3: string[] = ["red", "green", "blue"]
// colors3: string[]
// 字面量类型丢失!
// satisfies 最适合:既要约束,又要字面量类型
const config = {
host: "localhost",
port: 8080,
retry: 3,
verbose: true,
} satisfies Record<string, string | number | boolean>
// config 保留了每个属性的精确字面量类型┌─────────────────────────────────────────────────────────────┐
│ satisfies 使用场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 既需要类型检查,又需要保留字面量类型 │
│ → 如颜色列表、路由定义、配置常量 │
│ │
│ ✅ 验证一个对象满足某个接口,但不拓宽类型 │
│ → 如组件的 props 默认值 │
│ │
│ ❌ 如果不需要保留字面量类型,直接使用类型注解更清晰 │
│ → 如函数参数、模块导出 │
│ │
└─────────────────────────────────────────────────────────────┘12.3 const 类型参数(TS 5.0+)
// 在泛型参数前加 const 修饰符,等价于调用时自动加 as const
function useState<const T>(initial: T): [T, (v: T) => void] {
/* ... */
}
const [count, setCount] = useState(0)
// count: 0(字面量类型),而不是 number
// 对比:不加 const 修饰符
function useState2<T>(initial: T): [T, (v: T) => void] {
/* ... */
}
const [count2] = useState2(0)
// count2: number第13部分:工具类型速查与实践
13.1 属性操作类
interface User {
id: string
name: string
email?: string
readonly createdAt: Date
}
// Partial<T> —— 所有属性变为可选
type UserPatch = Partial<User>
// { id?: string; name?: string; email?: string;
// readonly createdAt?: Date }
// Required<T> —— 所有属性变为必需
type FullUser = Required<User>
// { id: string; name: string; email: string;
// readonly createdAt: Date }
// Readonly<T> —— 所有属性变为只读
type ImmutableUser = Readonly<User>
// { readonly id: string; readonly name: string;
// readonly email?: string; readonly createdAt: Date }
// Pick<T, K> —— 选取指定属性
type UserBrief = Pick<User, "id" | "name">
// { id: string; name: string }
// Omit<T, K> —— 排除指定属性
type UserWithoutEmail = Omit<User, "email">
// { id: string; name: string; readonly createdAt: Date }13.2 联合操作类
// Record<K, V> —— 以 K 为键、V 为值的对象类型
type PageRoutes = Record<"home" | "about" | "contact", string>
// { home: string; about: string; contact: string }
// Exclude<T, U> —— 从 T 中排除可赋值给 U 的类型
type Primitive = Exclude<string | number | Date | null, object | null>
// string | number
// Extract<T, U> —— 从 T 中提取可赋值给 U 的类型
type Obj = Extract<string | number | Date | null, object>
// Date
// NonNullable<T> —— 从 T 中排除 null 和 undefined
type RequiredValue = NonNullable<string | null | undefined>
// string13.3 函数类型操作类
// ReturnType<F> —— 提取函数返回类型
type FetchResult = ReturnType<typeof fetch>
// Promise<Response>
// Parameters<F> —— 提取函数参数元组
type FetchParams = Parameters<typeof fetch>
// [input: RequestInfo | URL, init?: RequestInit]
// Awaited<T> —— 解开 Promise 链
type FetchData = Awaited<ReturnType<typeof fetch>>
// Response(不是 Promise<Response>)
// 实战组合
declare function getUser(id: string): Promise<{ name: string; age: number }>
type UserResult = Awaited<ReturnType<typeof getUser>>
// { name: string; age: number }第14部分:声明文件与模块增强
14.1 .d.ts 文件的作用
┌─────────────────────────────────────────────────────────────┐
│ .d.ts 文件的三种用途 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 为 JS 库提供类型描述 │
│ → DefinitelyTyped (@types/*) 就是 .d.ts 的集合 │
│ │
│ 2. 声明全局类型和变量 │
│ → 如 process.env 的类型扩展 │
│ │
│ 3. 模块增强(Module Augmentation) │
│ → 为第三方库添加类型 │
│ │
│ 关键规则: │
│ .d.ts 中只有类型声明,不能有可执行代码 │
│ declare 关键字声明"存在于运行时但 TS 不知道的东西" │
│ │
└─────────────────────────────────────────────────────────────┘14.2 declare global 与全局扩展
// global.d.ts
declare global {
// 扩展全局变量
var __VERSION__: string
// 扩展 Window
interface Window {
__INITIAL_STATE__: Record<string, unknown>
}
// 扩展 Array(谨慎使用)
interface Array<T> {
last(): T | undefined
}
}
// 让文件作为模块(否则 declare global 在某些配置下失效)
export {}14.3 模块增强
// 为第三方库 express 添加自定义属性
declare module "express" {
interface Request {
currentUser?: {
id: string
role: "admin" | "user"
}
}
}
// 声明非 JS/TS 模块
declare module "*.svg" {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>
export default content
}
declare module "*.css" {
const classes: Record<string, string>
export default classes
}第15部分:品牌类型 —— 在结构类型中模拟名义类型
15.1 为什么要品牌类型
┌─────────────────────────────────────────────────────────────┐
│ 品牌类型解决的问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题:两个 string 可能是完全不同的东西 │
│ │
│ function transferMoney( │
│ fromAccountId: string, │
│ toAccountId: string, │
│ amount: number │
│ ) { ... } │
│ │
│ // 以下调用都能编译通过,但可能错误 │
│ transferMoney(userId, accountId, amount) // userId 不是 accountId
│ transferMoney(accountId, userName, amount) // userName 不是 accountId
│ │
│ 品牌类型解决: │
│ transferMoney( │
│ fromAccountId: AccountId, │
│ toAccountId: AccountId, │
│ amount: Money │
│ ) { ... } │
│ // → 编译时就能发现参数错位 │
│ │
└─────────────────────────────────────────────────────────────┘15.2 实现品牌类型
// 品牌类型的核心:交叉一个独特的品牌属性
type Brand<T, B> = T & { __brand: B }
type UserId = Brand<string, "UserId">
type AccountId = Brand<string, "AccountId">
type Money = Brand<number, "Money">
function createUserId(id: string): UserId {
return id as UserId // 唯一的品牌创建点
}
function createAccountId(id: string): AccountId {
return id as AccountId
}
function transferMoney(from: AccountId, to: AccountId, amount: Money): void {
// 现在类型安全了
}
// 使用
const acc1 = createAccountId("acc-001")
const acc2 = createAccountId("acc-002")
const amt = 100 as Money
transferMoney(acc1, acc2, amt) // ✅
// transferMoney(createUserId("u-1"), acc2, amt) // ❌ UserId != AccountId┌─────────────────────────────────────────────────────────────┐
│ 品牌类型的适用场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ ID 类型(UserId, OrderId, ProductId) │
│ ✅ 单位类型(Meters, Seconds, Pixels) │
│ ✅ 验证后的数据(ValidatedEmail, SanitizedHtml) │
│ ✅ 货币类型(USD, EUR, CNY) │
│ │
│ ⚠️ 品牌在运行时不存在,序列化后丢失 │
│ → 序列化/反序列化时需要重新品牌化 │
│ │
│ ⚠️ 不能阻止所有误用(如 as 强制转换) │
│ → 品牌类型是"护栏"而非"安全门" │
│ │
└─────────────────────────────────────────────────────────────┘第16部分:tsconfig.json 深度解析
16.1 strict 及其子选项
strict: true 是 TypeScript 推荐的默认起点。它开启以下全部子选项:
┌─────────────────────────────────────────────────────────────┐
│ strict 家族 │
├─────────────────────────────────────────────────────────────┤
│ │
│ strict: true 等价于同时开启: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ strictNullChecks null/undefined 不能赋给 │ │
│ │ 非空类型 │ │
│ │ strictFunctionTypes 函数参数双向协变检查 │ │
│ │ strictBindCallApply bind/call/apply 参数检查 │ │
│ │ strictPropertyInitialization 类属性必须初始化 │ │
│ │ noImplicitAny 禁止隐式 any │ │
│ │ noImplicitThis 禁止隐式 this 类型 │ │
│ │ alwaysStrict 输出 "use strict" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 建议:新项目直接 strict: true,不要逐个关闭子选项 │
│ │
└─────────────────────────────────────────────────────────────┘16.2 重要但非默认的选项
{
"compilerOptions": {
// ===== 强烈推荐 =====
// 索引访问时包含 undefined(数组越界的真相)
"noUncheckedIndexedAccess": true,
// 可选属性不允许显式赋 undefined
// { x?: number } 中 x 只能是 number 或省略,不能是 undefined
"exactOptionalPropertyTypes": true,
// ===== 模块相关 =====
// 现代项目的模块语法
"module": "ESNext",
"moduleResolution": "bundler", // 或 "node16" / "nodenext"
// 确保 import 类型和值的语法明确
"verbatimModuleSyntax": true,
// ===== 其他质量选项 =====
// 未使用的局部变量报错
"noUnusedLocals": true,
// 未使用的参数报错
"noUnusedParameters": true,
// 有返回类型的函数必须所有分支都有返回值
"noImplicitReturns": true,
// switch 的 fall-through 报错
"noFallthroughCasesInSwitch": true,
// 捕获的变量必须使用
"noUncheckedSideEffectImports": true,
}
}16.3 moduleResolution 的选择
┌─────────────────────────────────────────────────────────────┐
│ moduleResolution 选择指南 │
├─────────────────────────────────────────────────────────────┤
│ │
│ bundler(推荐,TS 5.0+) │
│ → 模拟打包器(webpack/vite/esbuild)的解析行为 │
│ → 支持无扩展名导入、package.json exports │
│ → 适合绝大多数现代前端项目 │
│ │
│ node16 / nodenext │
│ → 严格遵循 Node.js ESM/CJS 解析规则 │
│ → 必须使用 .js 扩展名导入 │
│ → 适合 Node.js 库和工具 │
│ │
│ node(遗留) │
│ → 旧版 Node.js 解析(CJS 为主) │
│ → 不支持 package.json exports │
│ → 不推荐新项目使用 │
│ │
└─────────────────────────────────────────────────────────────┘第17部分:运行时验证边界 —— 类型系统的边界
17.1 TypeScript 类型在运行时消失
这是 TypeScript 类型系统最根本的边界:编译后所有类型注解被擦除,运行时没有任何类型信息。
┌─────────────────────────────────────────────────────────────┐
│ 编译前后对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 编译前(.ts): │
│ interface User { │
│ id: string │
│ name: string │
│ } │
│ function greet(user: User): string { │
│ return `Hello, ${user.name}` │
│ } │
│ │
│ 编译后(.js): │
│ function greet(user) { │
│ return `Hello, ${user.name}` │
│ } │
│ // interface User 完全消失 │
│ // 参数类型注解完全消失 │
│ │
└─────────────────────────────────────────────────────────────┘17.2 必须运行时验证的数据源
以下数据在编译时类型未知,必须在运行时验证:
- HTTP 请求与响应(REST API、WebSocket)
- 环境变量(
process.env) - 数据库查询结果和历史数据
- 消息队列事件(Kafka、RabbitMQ、SQS)
- 本地存储(localStorage、IndexedDB)
- 第三方 SDK 返回值
- 用户输入、文件上传、URL 参数
17.3 Zod / Valibot 验证模式
┌─────────────────────────────────────────────────────────────┐
│ Schema 验证 + 类型推导模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ │
│ 外部数据 ────────→│ Zod Schema│────→ 验证通过的数据 │
│ (unknown) └───────────┘ (类型安全) │
│ │ │
│ │ z.infer<typeof schema> │
│ ↓ │
│ 编译时类型 │
│ (单一真源头) │
│ │
│ 核心思想:schema 是唯一的真相来源 │
│ 类型从 schema 推导,而非手写 interface 再单独验证 │
│ │
└─────────────────────────────────────────────────────────────┘import { z } from "zod"
// 1. 定义 schema(运行时 + 编译时的单一真相来源)
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email().optional(),
role: z.enum(["admin", "user", "viewer"]),
createdAt: z.string().datetime(),
})
// 2. 从 schema 推导类型(编译时)
type User = z.infer<typeof UserSchema>
// { id: string; name: string; email?: string;
// role: "admin" | "user" | "viewer"; createdAt: string }
// 3. 运行时验证
async function handleRequest(body: unknown): Promise<Response> {
const result = UserSchema.safeParse(body)
if (!result.success) {
return Response.json(
{ error: "Validation failed", details: result.error.issues },
{ status: 400 }
)
}
// result.data 的类型是 User,完全类型安全
const user: User = result.data
return Response.json({ user })
}// Valibot 的等价写法(tree-shakeable,适合前端)
import * as v from "valibot"
const UserSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
name: v.pipe(v.string(), v.minLength(1)),
email: v.optional(v.pipe(v.string(), v.email())),
role: v.picklist(["admin", "user", "viewer"]),
createdAt: v.pipe(v.string(), v.isoTimestamp()),
})
type User = v.InferOutput<typeof UserSchema>核心总结
类型系统设计哲学
┌─────────────────────────────────────────────────────────────┐
│ TypeScript 类型系统的三大支柱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 结构类型(Structural Typing) │
│ → 形状决定兼容性,而非名称或继承 │
│ → 最小约束原则:只声明真正需要的字段 │
│ │
│ 2. 类型收窄(Type Narrowing) │
│ → typeof / instanceof / in / 判别联合 / 类型谓词 │
│ → 从宽类型逐步收窄到精确类型 │
│ │
│ 3. 类型编程(Type-Level Programming) │
│ → keyof / typeof / 索引访问 / 映射 / 条件 / infer │
│ → 在类型层面表达复杂约束和转换 │
│ │
└─────────────────────────────────────────────────────────────┘类型系统边界
┌─────────────────────────────────────────────────────────────┐
│ 类型安全的完整图景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 编译时(TypeScript) │ 运行时(JavaScript) │
│ │ │
│ ┌──────────────────┐ │ ┌──────────────────────┐ │
│ │ 类型注解 │ │ │ Schema 验证(Zod) │ │
│ │ 类型推断 │ │ │ 边界检查 │ │
│ │ 泛型约束 │ │ │ 防御性编程 │ │
│ │ 类型收窄 │ │ │ try-catch │ │
│ │ 条件类型 │ │ │ 默认值处理 │ │
│ └──────────────────┘ │ └──────────────────────┐ │
│ ↓ │ ↓ │ │
│ 保证内部逻辑正确 │ 保证外部数据安全 │ │
│ │ │
│ 两者互补,缺一不可 │
│ │
└─────────────────────────────────────────────────────────────┘最佳实践速查
┌─────────────────────────────────────────────────────────────┐
│ TypeScript 最佳实践清单 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ strict: true 是起点,不是目标 │
│ ✅ 局部变量靠推断,函数边界写类型 │
│ ✅ unknown > any(99%的场景) │
│ ✅ 用判别联合建模状态,不用布尔标志的组合 │
│ ✅ 泛型表达关系,不是减少字符 │
│ ✅ 对象形状用 interface,类型运算用 type │
│ ✅ 跨界数据必须运行时验证(Zod/Valibot) │
│ ✅ 不要共用一个万能类型——数据库行 ≠ API 响应 ≠ 表单 │
│ ✅ public API 类型保持简单——让调用方容易理解错误 │
│ ✅ as const / satisfies 保留字面量类型 │
│ │
└─────────────────────────────────────────────────────────────┘章节测试
选择题
TypeScript 的类型系统属于哪一类? A. 名义类型 B. 结构类型 C. 鸭子类型 D. 强类型
以下哪项不是
as const的效果? A. 属性变为readonlyB. 字面量类型收窄到精确值 C. 数组变为只读元组 D. 对象变为不可扩展关于泛型的"只用一次"启发式,以下哪种用法是正确的? A.
function foo<T>(x: T): void—— T 只用于参数类型 B.function map<T, U>(arr: T[], fn: (v: T) => U): U[]C.function bar<T extends string>(x: T): T—— T 只在参数中 D.function logger<T>(data: T): void—— T 只用于参数以下哪种情况不能通过
keyof获取键的联合类型? A.interfaceB.type别名 C. 联合类型 D.class[T] extends [any] ? T[] : never中,方括号的作用是什么? A. 强制 T 为数组 B. 阻止条件类型的分配 C. 提取元组元素 D. 类型断言
简答题
简述
unknown和any的区别,并说明何时必须使用unknown。写出一个判别联合类型来建模 HTTP 请求状态(idle / pending / success / error),并为每个变体提供合适的字段。
参考答案
B. 结构类型 —— TypeScript 使用结构类型系统,类型兼容性由形状决定。
D. 对象变为不可扩展 ——
as const不会阻止向对象添加新属性(在运行时),它只影响编译时的类型推断。B. ——
T和U都出现了至少两次,表达了数组元素类型到返回数组元素类型的映射关系。A/C/D 的泛型参数各只出现一次,没有建立关系。C. 联合类型 ——
keyof作用于对象类型,联合类型没有固定的键集合。keyof (A | B)的结果是keyof A和keyof B的共有键。B. 阻止条件类型的分配 —— 当 T 是裸泛型参数且传入联合类型时,条件类型会分配。用
[T]包裹后 T 不再是裸参数,分配行为被抑制。any关闭所有类型检查,可以从它访问任意属性、赋给任意类型,不会有编译错误。unknown是类型安全的顶层类型:不能直接使用,必须先通过类型收窄确定具体类型。使用场景:处理外部输入(API 响应、用户输入、JSON.parse)时,先用unknown接收,再通过 schema 验证或类型守卫收窄到具体类型。- ts
type HttpState<T> = | { status: "idle" } | { status: "pending"; abortController: AbortController } | { status: "success"; data: T; statusCode: number } |
---
## 相关笔记
- [[01-javascript-runtime-and-language]] —— JavaScript 运行时模型和语言核心语义,是 TypeScript 的运行时基础
- [[03-async-and-event-loop]] —— 异步编程中 `Promise<T>` 的类型建模与错误处理模式
---
## 下一步学习
- [ ] 在新项目中启用 `strict: true` + `noUncheckedIndexedAccess: true` + `exactOptionalPropertyTypes: true`
- [ ] 将项目中的 `any` 逐一替换为 `unknown` 或具体类型
- [ ] 为一个现有的 API 响应类型添加 Zod schema 验证层
- [ ] 用判别联合重构项目中一处用布尔标志组合建模的状态
- [ ] 阅读 [TypeScript Handbook - Type Manipulation](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html)
- [ ] 阅读 [type-challenges](https://github.com/type-challenges/type-challenges) 并完成前 10 道 Easy 题目
**学习状态**:🟡 开始学习