文章目录

TypeScript 进阶学习笔记

适合目标:5 小时内快速建立 TypeScript 进阶类型系统框架,覆盖高级类型、工具类型实现、类型体操与高频面试题。
学习重点:泛型约束、条件类型、映射类型、模板字面量类型、工具类型实现、类型体操题。
学习原则:先理解“类型在做什么”,再去背写法;先会看懂,再会自己实现;先掌握套路,再练复杂题。


目录

  1. 学习总览
  2. TypeScript 类型系统基础补强
  3. 高级类型
  4. 工具类型实现
  5. 类型体操面试题
  6. 实战场景与思维方式
  7. 高频面试题
  8. 5 小时学习节奏建议
  9. 一页速记总结
  10. 背诵口诀

1. 学习总览

1.1 TypeScript 到底在学什么

很多人学 TypeScript 会有一种感觉:

  1. 代码看起来像 JavaScript,但又多了很多陌生语法
  2. 类型语法能看懂一点,但自己写就容易卡
  3. 工具类型会用,换个题就不会实现

本质上,TypeScript 学的不是“语法堆积”,而是下面这套思维:

值的世界 -> 类型的世界 -> 类型之间如何推导 -> 如何约束和转换类型

也就是说,你要慢慢形成一个认知:

  1. JavaScript 在运行值
  2. TypeScript 在编译阶段分析类型
  3. 很多“高级类型”本质是对类型做判断、遍历、拼接、提取、过滤

1.2 你可以把 TS 类型系统看成什么

可以把 TypeScript 类型系统理解成:

一个只在编译期运行的小型逻辑系统

它最常做的事情只有几类:

  1. 接收一个类型
  2. 判断它是否满足某条件
  3. 把它映射成另一个类型
  4. 从里面提取一部分信息
  5. 把多个类型拼起来生成新类型

1.3 核心主线

主线 1:泛型解决“类型如何复用”

  • <T>:把类型当参数传进去
  • extends:给泛型加约束
  • = 默认值:给泛型默认类型

主线 2:条件类型解决“类型如何判断”

  • T extends U ? X : Y
  • if...else,但判断的是类型
  • 常与 infer 配合做提取

主线 3:映射类型解决“类型如何遍历”

  • [K in keyof T]
  • 遍历对象每个 key
  • 常用于实现 PartialReadonlyPick

主线 4:模板字面量类型解决“类型如何拼接字符串”

  • `prefix${T}`
  • 常用于事件名、路由名、状态名推导

主线 5:类型体操解决“综合推导”

  • 深度递归
  • 联合分发
  • 元组拆解
  • 链式调用累积类型

2. TypeScript 类型系统基础补强

你给的重点是进阶内容,但如果基础概念没稳,后面会越看越绕。所以这里先补齐最关键的地基,只保留和进阶题最相关的部分。

2.1 类型和值要分开理解

TypeScript 里最容易混淆的一点是:

  1. 值存在于运行时
  2. 类型存在于编译时

示例:

const name = "tom";
type Name = string;

这里:

  • name 是值
  • Name 是类型

TypeScript 会在编译时检查 name 是否符合 Name 之类的规则,但运行时不会保留这些类型信息。

2.2 常见基础类型要非常清楚

any

any 表示关闭类型检查,尽量少用。

let value: any = 123;
value = "abc";
value.foo.bar.baz();

问题:

  1. 可以赋值给任何类型
  2. 也可以接收任何类型
  3. 等于放弃了 TypeScript 的价值

unknown

unknown 表示“未知类型”,比 any 安全。

let value: unknown = "hello";

if (typeof value === "string") {
  console.log(value.toUpperCase());
}

区别:

  1. any 可以直接乱用
  2. unknown 必须先缩小类型后再使用

never

never 表示“永远不会有值”。

常见场景:

  1. 永远抛错的函数
  2. 死循环函数
  3. 条件类型中过滤分支
  4. 穷尽性检查
function throwError(message: string): never {
  throw new Error(message);
}

void

void 表示函数没有返回值。

function log(message: string): void {
  console.log(message);
}

2.3 联合类型与交叉类型

联合类型

type Status = "success" | "error" | "loading";

含义:

值可能是其中之一。

交叉类型

type A = { name: string };
type B = { age: number };
type C = A & B;

含义:

C 同时拥有 AB 的结构。

2.4 type 和 interface 区别

type

更灵活,适合:

  1. 联合类型
  2. 交叉类型
  3. 条件类型
  4. 工具类型实现

interface

更偏向描述对象结构,支持声明合并。

interface User {
  name: string;
}

interface User {
  age: number;
}

const u: User = {
  name: "tom",
  age: 18
};

面试怎么答

  1. 两者都能描述对象结构
  2. type 更适合复杂类型表达式
  3. interface 支持声明合并,扩展对象语义更自然
  4. 实际项目里通常对象优先 interface,复杂组合优先 type

2.5 keyof、typeof、in、索引访问

这些是后面所有高级类型的基础。

keyof

获取对象类型的 key 联合。

type User = {
  name: string;
  age: number;
};

type UserKeys = keyof User; // "name" | "age"

typeof

在类型上下文里获取某个值的类型。

const user = {
  name: "tom",
  age: 18
};

type User = typeof user;

索引访问类型

type User = {
  name: string;
  age: number;
};

type NameType = User["name"]; // string

in

在映射类型中遍历 key。

type Copy<T> = {
  [K in keyof T]: T[K];
};

2.6 extends 在 TS 里有两种常见含义

这个点很容易混。

含义 1:泛型约束

type GetName<T extends { name: string }> = T["name"];

表示:

T 必须满足某结构。

含义 2:条件判断

type IsString<T> = T extends string ? true : false;

表示:

如果 T 可以赋给 string,返回 true,否则 false

记忆口诀

写在尖括号里,多半是约束;写在问号前面,多半是判断。


3. 高级类型

这一部分是 TypeScript 进阶的核心,也是你图片里第一大块的重点。

3.1 泛型

3.1.1 什么是泛型

泛型就是“把类型参数化”,让类型像函数参数一样可复用。

function identity<T>(value: T): T {
  return value;
}

const a = identity<string>("hello");
const b = identity(123);

理解:

  1. T 不是具体类型
  2. 调用时再由传入值推导或显式指定
  3. 让函数、接口、类型别名具备复用能力

3.1.2 泛型为什么重要

如果不用泛型,你常常只能:

  1. 写死类型
  2. 或者写成 any

泛型的价值就是:

既灵活,又保留类型信息

3.1.3 泛型约束

你图片里的重点之一就是:

<T extends Constraint>

含义:

泛型参数 T 必须满足某种结构或范围。

示例:

type GetLength<T extends { length: number }> = T["length"];

这个类型要求:

T 必须有 length 属性。

函数版示例:

function getLength<T extends { length: number }>(value: T) {
  return value.length;
}

getLength("abc");
getLength([1, 2, 3]);
// getLength(123); // 报错

3.1.4 keyof 约束

这是面试高频:

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

理解:

  1. T 表示对象类型
  2. K extends keyof T 表示 key 必须是对象已有的键
  3. 返回值 T[K] 表示对应属性值类型

3.1.5 泛型默认值

图片里有“泛型默认值怎么写”,标准写法是:

type Box<T = string> = {
  value: T;
};

使用:

type A = Box;        // 默认 T = string
type B = Box<number>;

泛型默认值常见场景:

  1. 减少调用方书写成本
  2. 提供常用默认类型
  3. 让 API 更友好

3.1.6 多个泛型参数

type Pair<T, U = T> = {
  first: T;
  second: U;
};

这里 U = T 表示:

如果没传第二个类型参数,就默认和 T 一样。

3.1.7 泛型记忆口诀

泛型是类型的变量,extends 负责加门槛,= 负责给默认。


3.2 条件类型

3.2.1 条件类型是什么

条件类型就是类型系统里的 if...else

写法:

T extends U ? X : Y

含义:

如果 T 可以赋值给 U,结果就是 X,否则是 Y

3.2.2 最简单例子

type IsString<T> = T extends string ? true : false;

type A = IsString<"abc">; // true
type B = IsString<number>; // false

3.2.3 实现 If 条件类型

这是面试常见题:

type If<C extends boolean, T, F> = C extends true ? T : F;

type A = If<true, "yes", "no">;  // "yes"
type B = If<false, "yes", "no">; // "no"

3.2.4 条件类型的本质

它通常用来做四件事:

  1. 判断类型是否满足某条件
  2. 根据判断结果返回不同类型
  3. 过滤联合类型
  4. 配合 infer 提取类型

3.2.5 分发条件类型

这是 TypeScript 进阶里最关键的点之一。

当条件类型左边是“裸类型参数”并且传入联合类型时,会自动分发。

示例:

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;

结果:

type Result = string[] | number[];

因为它被拆成:

type Result = ToArray<string> | ToArray<number>;

3.2.6 如何阻止分发

把类型参数包起来:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result = ToArrayNonDist<string | number>; // (string | number)[]

3.2.7 Exclude 和 Extract 的本质

type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;

理解:

  1. Exclude 是把满足条件的剔掉
  2. Extract 是把满足条件的留下

3.2.8 条件类型记忆口诀

条件类型像 if,联合类型会分发,never 常用来过滤。


3.3 infer 类型推断

3.3.1 infer 是什么

infer 可以在条件类型中“声明一个待推导的类型变量”,用来从某个复杂类型里提取一部分信息。

你可以把它理解成:

条件成立时,顺手把内部某段类型抓出来

3.3.2 提取函数返回值

type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

解释:

  1. 先约束 T 必须是函数
  2. 如果 T 形如 (...args) => 某返回值
  3. 那就把返回值推断成 R
  4. 最终返回 R

使用:

type Fn = (name: string) => number;
type Result = MyReturnType<Fn>; // number

3.3.3 提取参数类型

type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

结果是元组类型。

type Fn = (name: string, age: number) => boolean;
type Params = MyParameters<Fn>; // [name: string, age: number]

3.3.4 提取 Promise 内部值

type MyAwaited<T> = T extends Promise<infer R> ? R : T;

更完整的递归版:

type MyAwaitedDeep<T> =
  T extends Promise<infer R> ? MyAwaitedDeep<R> : T;

3.3.5 从元组中取第一项

type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

3.3.6 infer 记忆口诀

infer 只在条件类型里用,作用是把类型里的某部分抠出来。


3.4 映射类型

3.4.1 映射类型是什么

映射类型就是:

遍历一个对象类型的所有 key,生成新的对象类型

基本写法:

type Copy<T> = {
  [K in keyof T]: T[K];
};

3.4.2 Partial 的本质

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

作用:

把对象所有属性都变成可选。

3.4.3 Readonly 的本质

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

3.4.4 Required 的本质

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

这里 -? 表示移除可选修饰符。

3.4.5 可选和只读修饰符操作

TypeScript 映射类型里常见修饰符:

  1. ?:加可选
  2. -?:去可选
  3. readonly:加只读
  4. -readonly:去只读

示例:

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

3.4.6 Pick 的实现

图片里给的手写题之一就是:

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

理解:

  1. K 只能是 T 的 key
  2. 遍历选中的 K
  3. 值类型仍取 T[P]

3.4.7 Omit 的实现

type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

理解:

  1. keyof T 取出全部 key
  2. Exclude<keyof T, K> 去掉要省略的部分
  3. 再重新映射回对象类型

3.4.8 Record 的实现

type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

说明:

  1. K 可以是字符串、数字、symbol 这类合法 key
  2. 每个 key 的值都统一为 T

3.4.9 映射类型记忆口诀

keyof 先拿键,in 去遍历,T[K] 拿值,修饰符控制可选和只读。


3.5 模板字面量类型

3.5.1 什么是模板字面量类型

模板字面量类型允许你像拼字符串一样拼类型。

type EventName<T extends string> = `on${Capitalize<T>}`;

type A = EventName<"click">; // "onClick"

3.5.2 为什么它重要

它特别适合处理:

  1. 事件名推导
  2. CSS 类名推导
  3. 路由 key 推导
  4. getter/setter 命名
  5. API 前缀拼接

3.5.3 事件名推导示例

type Events = "click" | "change" | "focus";
type HandlerNames = `on${Capitalize<Events>}`;

结果:

type HandlerNames = "onClick" | "onChange" | "onFocus";

3.5.4 结合 keyof 使用

type Watchers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};

示例:

type User = {
  name: string;
  age: number;
};

type UserWatchers = Watchers<User>;

会得到:

type UserWatchers = {
  onNameChange: (value: string) => void;
  onAgeChange: (value: number) => void;
};

3.5.5 内置字符串工具类型

  1. Uppercase<T>
  2. Lowercase<T>
  3. Capitalize<T>
  4. Uncapitalize<T>

3.5.6 模板字面量类型记忆口诀

模板字面量类型就是把字符串拼接能力搬到了类型系统。


3.6 高级类型串联理解

把这些内容串起来,你会发现很多题目本质都只有一个套路:

  1. 先拿到 key 或结构
  2. 再判断是否满足条件
  3. 必要时用 infer 提取内部类型
  4. 最后用映射或模板字面量生成新类型

举个综合例子:

type EventMap<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};

这里同时用到了:

  1. keyof
  2. 映射类型
  3. as 重映射 key
  4. 模板字面量类型
  5. Capitalize

4. 工具类型实现

这一部分是你图片里的第二大块,也是面试里最容易考“手写”的内容。

4.1 为什么要学工具类型实现

因为会“用”和会“实现”是两个层次:

  1. 会用说明你知道场景
  2. 会实现说明你理解类型系统底层套路

只要你掌握了工具类型的实现思路,很多类型体操题都会变简单。


4.2 Pick

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

记忆拆解:

  1. K extends keyof T:只能选已有键
  2. [P in K]:遍历这些键
  3. T[P]:取原对象中对应属性值类型

4.3 Omit

type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

记忆拆解:

  1. keyof T:先拿全部 key
  2. Exclude<keyof T, K>:删掉不想要的
  3. 再映射回对象

4.4 Partial

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

核心:

? 把每个属性变可选。


4.5 Readonly

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

核心:

readonly 给每个属性加只读。


4.6 Required

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

核心:

-? 去掉可选修饰符。


4.7 Record

type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

适合:

已知 key 集合,想统一映射成同类型值。

type Roles = "admin" | "user";
type RoleMap = MyRecord<Roles, boolean>;

4.8 Exclude

type MyExclude<T, U> = T extends U ? never : T;

例子:

type A = MyExclude<"a" | "b" | "c", "a" | "b">; // "c"

本质:

条件类型 + 联合分发 + never 过滤。


4.9 Extract

type MyExtract<T, U> = T extends U ? T : never;

例子:

type A = MyExtract<"a" | "b" | "c", "a" | "b">; // "a" | "b"

本质:

保留满足条件的那部分。


4.10 NonNullable

type MyNonNullable<T> = T extends null | undefined ? never : T;

例子:

type A = MyNonNullable<string | null | undefined>; // string

4.11 ReturnType

type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

这是你图片里直接给出的重点之一。

记忆拆解:

  1. 先限制传入的是函数类型
  2. 用条件类型判断函数形状
  3. infer R 提取返回值

4.12 Parameters

type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

作用:

提取函数参数列表元组。


4.13 ConstructorParameters

type MyConstructorParameters<T extends abstract new (...args: any[]) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;

4.14 InstanceType

type MyInstanceType<T extends abstract new (...args: any[]) => any> =
  T extends abstract new (...args: any[]) => infer R ? R : never;

4.15 Awaited

type MyAwaited<T> =
  T extends Promise<infer R> ? MyAwaited<R> : T;

作用:

递归拆出 Promise 最终包裹的值。


4.16 工具类型实现总规律

几乎所有工具类型都能归纳为下面几种套路:

  1. 遍历对象:映射类型
  2. 过滤联合:条件类型 + never
  3. 提取信息:infer
  4. 拼接 key:模板字面量类型
  5. 深度处理:递归

5. 类型体操面试题

这一部分是你图片里的第三大块。不要把它当纯背题,而要把它当成“前面所有语法的综合应用”。

5.1 实现 DeepPartial

5.1.1 题目目标

把对象所有层级的属性都变成可选。

5.1.2 基础版实现

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

5.1.3 理解步骤

  1. 遍历 T 的每个 key
  2. 当前属性变可选
  3. 如果属性值还是对象,就递归处理
  4. 否则保留原值类型

5.1.4 注意点

简单写法里 object 会把数组、函数等也包含进去,实际项目里有时要做更细判断。

更稳一些的版本:

type DeepPartial<T> =
  T extends Function ? T :
  T extends Array<infer U> ? Array<DeepPartial<U>> :
  T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } :
  T;

5.1.5 记忆口诀

DeepPartial = Partial + 递归。


5.2 实现 DeepReadonly

5.2.1 题目目标

把对象所有层级属性都变成只读。

5.2.2 实现

type DeepReadonly<T> =
  T extends Function ? T :
  T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> :
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
  T;

5.2.3 思路

  1. 函数直接返回
  2. 数组递归成只读数组
  3. 对象层层递归加 readonly
  4. 基本类型直接返回

5.2.4 记忆口诀

DeepReadonly = Readonly + 递归。


5.3 实现 TupleToUnion

5.3.1 题目目标

把元组类型转成联合类型。

5.3.2 最简单写法

type TupleToUnion<T extends readonly any[]> = T[number];

5.3.3 为什么这么写就行

因为数组或元组的索引访问类型:

T[number]

表示“拿出这个元组所有数字索引位置的值类型”,自然就变成联合类型。

示例:

type A = TupleToUnion<["a", "b", "c"]>; // "a" | "b" | "c"

5.3.4 递归版

type TupleToUnion2<T extends any[]> =
  T extends [infer F, ...infer R] ? F | TupleToUnion2<R> : never;

5.3.5 记忆口诀

元组转联合,优先想到 T[number]。


5.4 实现 Chainable

5.4.1 题目背景

这是类型体操非常经典的一题,目的是实现:

config
  .option("name", "tom")
  .option("age", 18)
  .get();

并让最终类型变成:

{
  name: string;
  age: number;
}

5.4.2 实现

type Chainable<T = {}> = {
  option<K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<T & { [P in K]: V }>;
  get(): T;
};

5.4.3 思路拆解

  1. 用泛型 T 保存当前累积结果
  2. 每次 option 调用时新增一个属性
  3. 返回新的 Chainable<新类型>
  4. get() 返回最终累积类型
  5. K extends keyof T ? never : K 用来防止重复 key

5.4.4 为什么要用交叉类型

T & { [P in K]: V }

表示:

把已有配置 T 和当前新增配置合并起来。

5.4.5 记忆口诀

链式调用题的关键是:每调一次,类型就累加一次。


5.5 再补几个高频类型体操题

实现 First

type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

实现 Last

type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

实现 Pop

type Pop<T extends any[]> = T extends [...infer R, any] ? R : never;

实现 Shift

type Shift<T extends any[]> = T extends [any, ...infer R] ? R : never;

实现 AppendArgument

type AppendArgument<Fn, A> =
  Fn extends (...args: infer P) => infer R ? (...args: [...P, A]) => R : never;

实现 Mutable

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

5.6 类型体操做题模板

看到一道题,优先按这个顺序想:

  1. 输入是对象、联合、函数、元组,还是字符串?
  2. 需要遍历吗?如果需要,先想映射类型
  3. 需要判断吗?如果需要,先想条件类型
  4. 需要提取一部分吗?如果需要,先想 infer
  5. 需要递归吗?如果需要,注意终止条件
  6. 需要过滤吗?优先想到 never
  7. 需要拼接字符串吗?优先想到模板字面量类型

6. 实战场景与思维方式

学高级类型最怕“只会题,不会用”。这一部分把类型系统放回实际开发里。

6.1 API 响应统一建模

type ApiResponse<T> = {
  code: number;
  message: string;
  data: T;
};

type User = {
  id: number;
  name: string;
};

type UserResponse = ApiResponse<User>;

意义:

  1. 统一接口返回结构
  2. 每个接口只关心 data 的具体类型
  3. 泛型真正落地

6.2 表单配置推导

type FormConfig<T> = {
  [K in keyof T]: {
    label: string;
    value: T[K];
  };
};

这样你只要定义业务对象,就能推导出对应表单结构。


6.3 事件名推导

type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]?: (value: T[K]) => void;
};

这类写法在:

  1. 组件事件
  2. 状态变更监听
  3. 插件系统 hook 命名

里都很常见。


6.4 状态机风格的联合类型

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; error: Error };

使用时配合缩小类型:

function render(state: RequestState) {
  switch (state.status) {
    case "success":
      return state.data.join(",");
    case "error":
      return state.error.message;
    default:
      return state.status;
  }
}

好处:

  1. 状态清晰
  2. 分支安全
  3. 比随便写多个可选字段更可靠

6.5 穷尽性检查

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function render(state: RequestState) {
  switch (state.status) {
    case "idle":
      return "idle";
    case "loading":
      return "loading";
    case "success":
      return state.data.join(",");
    case "error":
      return state.error.message;
    default:
      return assertNever(state);
  }
}

意义:

如果以后新增状态但没处理,TypeScript 会在编译期提醒你。


6.6 项目里该怎么写得更实用

  1. 能推导就别手写重复类型
  2. 能约束就别用 any
  3. 公共结构优先抽成泛型
  4. 联合类型比多个布尔值更清晰
  5. 类型体操别为了炫技,优先可读性

7. 高频面试题

7.1 什么是泛型,为什么需要泛型

参考回答:

  1. 泛型是把类型参数化,让函数、接口、类型别名在保持类型安全的同时具备复用能力
  2. 不使用泛型时,通常只能写死类型或退化成 any
  3. 泛型的核心价值是“既灵活,又保留信息”

7.2 泛型约束和泛型默认值怎么写

参考回答:

type A<T extends { id: number }> = T;
type B<T = string> = { value: T };

说明:

  1. extends 用于约束泛型必须满足某结构
  2. = 用于给泛型提供默认值

7.3 条件类型是什么

参考回答:

条件类型是类型层面的条件判断,写法为 T extends U ? X : Y。它常用于类型过滤、类型分支处理和配合 infer 做类型提取。


7.4 什么是分发条件类型

参考回答:

当条件类型左侧是裸类型参数,且传入的是联合类型时,TypeScript 会对联合中的每一项分别执行条件判断,再把结果合并成联合类型,这就是分发条件类型。


7.5 infer 的作用是什么

参考回答:

infer 只能在条件类型中使用,用来从某个复杂类型结构中提取一部分类型信息,比如函数返回值、参数列表、Promise 内部值等。


7.6 映射类型是什么

参考回答:

映射类型是通过 [K in keyof T] 遍历对象类型的所有属性,再生成一个新类型的机制,常见于 PartialReadonlyPick 等工具类型实现。


7.7 Pick 和 Omit 怎么实现

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

7.8 模板字面量类型有什么用

参考回答:

模板字面量类型可以在类型层面对字符串做拼接和变换,常用于事件名推导、路由名生成、getter/setter 命名以及结合 key 重映射生成更智能的 API 类型。


7.9 DeepPartial 和 Partial 的区别

参考回答:

Partial<T> 只会把第一层属性变成可选,而 DeepPartial<T> 会递归地把所有层级属性都变成可选。


7.10 如何理解 Chainable 这类题

参考回答:

这类题的本质是“每次方法调用都返回一个携带更多信息的新泛型实例”,核心是累积类型信息,而不是只写运行时代码。


8. 5 小时学习节奏建议

按你给的学习时长,这里给你一版适合快速上手和记忆的节奏安排。

第 1 小时:先把地基搭起来

学习目标:

  1. 分清类型和值
  2. 理解 typeinterface
  3. 熟悉 keyoftypeofin、索引访问
  4. 明确 extends 的两种含义

必须会:

  1. keyof T
  2. T[K]
  3. K extends keyof T

第 2 小时:高级类型主干

学习目标:

  1. 学会泛型
  2. 学会条件类型
  3. 学会 infer

必须会:

  1. 写泛型约束
  2. If<T>
  3. ReturnType

第 3 小时:映射类型和模板字面量类型

学习目标:

  1. 吃透 [K in keyof T]
  2. 理解修饰符 ?-?readonly
  3. 学会模板字面量类型拼接

必须会:

  1. Partial
  2. Readonly
  3. Pick
  4. 写事件名推导

第 4 小时:工具类型和类型体操

学习目标:

  1. 手写高频工具类型
  2. 练递归思维
  3. 理解类型累积

必须会:

  1. Omit
  2. ReturnType
  3. DeepPartial
  4. TupleToUnion
  5. Chainable

第 5 小时:复盘 + 讲解 + 默写

建议:

  1. 不看笔记自己写 8 个工具类型
  2. 自己解释 extendsinfer、分发条件类型
  3. 自己讲一遍 DeepPartialChainable

检验标准:

  1. 你能说出每一题在“遍历什么”
  2. 你能说出每一题在“判断什么”
  3. 你能说出 infer 在“提取什么”

9. 一页速记总结

9.1 泛型

  1. <T>:类型参数
  2. <T extends X>:类型约束
  3. <T = Default>:默认类型

9.2 条件类型

  1. T extends U ? X : Y
  2. 裸类型参数遇联合会分发
  3. never 常用于过滤

9.3 infer

  1. 只能在条件类型里用
  2. 常用于提取返回值、参数、Promise 内部值

9.4 映射类型

  1. [K in keyof T]
  2. T[K] 取值类型
  3. ?-?readonly-readonly 控制修饰符

9.5 模板字面量类型

  1. `prefix${T}`
  2. 常结合 Capitalizekeyofas 使用

9.6 工具类型套路

  1. Pick:挑 key
  2. Omit:去 key
  3. Exclude:过滤联合
  4. Extract:提取联合
  5. ReturnType:提取返回值
  6. Parameters:提取参数

9.7 类型体操套路

  1. 对象题:先想映射类型
  2. 联合题:先想条件类型
  3. 函数题:先想 infer
  4. 元组题:先想解构 infer
  5. 深度题:先想递归

10. 背诵口诀

10.1 总口诀

泛型做复用,条件做判断,infer 做提取,映射做遍历,模板做拼接,递归做深度。

10.2 泛型口诀

尖括号里放类型,extends 加限制,等号给默认。

10.3 条件类型口诀

extends 像 if,联合会分发,never 专门过滤。

10.4 映射类型口诀

keyof 拿键,in 遍历,T[K] 取值,修饰符改属性。

10.5 infer 口诀

看见复杂结构想提取,优先想到 infer。

10.6 类型体操口诀

先看输入像什么,再看要遍历还是提取,最后考虑递归。


11. 自测题

试着不看答案,自己回答下面问题:

  1. extends 在泛型约束和条件类型里有什么区别?
  2. 为什么 K extends keyof T 能保证 key 安全?
  3. 什么是分发条件类型,怎么阻止它?
  4. ExcludeExtract 为什么都离不开 never
  5. infer 为什么只能写在条件类型里?
  6. PickOmit 的实现核心分别是什么?
  7. PartialDeepPartial 的本质差别是什么?
  8. 为什么 TupleToUnion<T> 可以直接写成 T[number]
  9. Chainable 题里为什么每次都要返回一个新的泛型类型?
  10. 模板字面量类型为什么特别适合事件名推导?

12. 面试回答总模板

回答类型题时,推荐按这个顺序说:

  1. 先说它解决什么问题
  2. 再说核心语法结构
  3. 再说底层思路
  4. 最后举一个简单例子

例如回答 ReturnType

  1. 它用来提取函数返回值类型
  2. 写法依赖条件类型和 infer
  3. 核心是判断传入类型是否为函数,并从函数签名中推导出返回值
  4. 所以 type R = ReturnType<() => string> 的结果就是 string

这个模板可以套在:

  1. 泛型约束
  2. 条件类型
  3. 映射类型
  4. 模板字面量类型
  5. 各类工具类型

13. 最后给你的学习建议

如果你想快速学会并记住 TypeScript,不要上来就死磕特别绕的体操题。更高效的顺序是:

  1. 先掌握 keyofT[K]in
  2. 再掌握泛型、条件类型、infer
  3. 再写 PickOmitReturnType
  4. 最后再做 DeepPartialTupleToUnionChainable

真正学会的标准不是“看懂答案”,而是:

  1. 你能自己拆出题型
  2. 你能自己判断该用哪种语法
  3. 你能自己写出第一版实现
  4. 你能解释为什么这样写

把 TypeScript 真正学明白的关键,不是记住多少题,而是记住这几个动作:

  1. 约束
  2. 判断
  3. 提取
  4. 遍历
  5. 拼接
  6. 递归

只要你看到题目能立刻判断“这是在做哪一种动作”,TypeScript 进阶就开始顺了。