TypeScript 指南

52 阅读11分钟

interface & type

两者都能定义对象结构和函数签名,但interface 更偏向结构扩展与 OOP 语义;type 更偏向类型组合与类型表达式。

在工程中,interface 用于描述数据结构,type 用于类型运算和复杂组合。

相同点

  • 都能描述对象形状
  • 都能做函数类型定义
  • 都支持泛型
  • 都能被 class implements
interface A { x: number }
type B = { x: number }

核心区别

  1. interface 可以合并声明
interface User { id: number }
interface User { name: string }

const u: User = { id: 1, name: 'a' }

👉 type 不行,这是 interface 最重要的能力。

适合用来扩展第三方库定义(例如扩展 Express Request)。

  1. type 更灵活,支持类型表达式

type 可以:

  • 联合
  • 交叉
  • 条件类型
  • 映射类型
  • 模板字符串类型
type Status = 'success' | 'error';
type WithId<T> = T & { id: string };
type Api<T> = T extends any[] ? 'array' : 'object';

👉 interface 做不到这些。

  1. interface 更适合建模“对象、类、API”结构
interface Person {
  name: string;
  say(): void;
}

它能:

  • 扩展其他 interface(extends)
  • 被类 implements(非常 OOP)
  1. type 可以“别名化”(alias)任何类型
type Fn = (x: number) => void;
type Point = [number, number];
type Maybe<T> = T | undefined;

👉 type 的使用范围更广,甚至可以 alias 基本类型。
interface 做不到。

  1. 扩展方式不同(extends vs 交叉类型)
  • interface 扩展
interface A { x: number }
interface B extends A { y: number }
  • type 扩展
type A = { x: number }
type B = A & { y: number }

两者效果一致,但交叉类型能做更多复杂组合。

  1. 复杂类型构造:type 更强

前端工程里的工具类型、复杂泛型、联合类型转换 几乎都是 type 实现的

type Partial<T> = {
  [P in keyof T]?: T[P];
}

interface 无法实现这种类型运算。

工程中如何选择

如果是 描述数据结构、业务对象、class API,优先用 interface,因为其更可扩展、可合并,团队协作更友好。

如果是 类型组合、联合类型、工具类型、映射类型、模板字面量,一定用 type,因为更灵活。

unknow & any & never

🐱unknown

unknown 是安全的 any,可以接受任何值,但不能被直接使用,需要类型收窄。适用于以下的严格使用场景。

  1. 不信任来源的数据(API、用户输入、动态数据)
function parse(json: string): unknown {
  return JSON.parse(json);
}
  1. 泛型上界不确定,但不希望失去类型安全
function handle<T extends unknown>(value: T) {}
  1. 设计库时需要“类型安全的扩展点”

比如写 SDK、插件系统时,unknown 让调用方必须显式确认类型。

🐶never

never 代表“不可能发生的值”(类型系统的底部类型)

  • 程序不会走到这里
  • 函数不会返回
  • 分支被完全排除

适用于以下的严格使用场景。

  1. 表示函数永远不返回(异常 / 死循环)
function fail(msg: string): never {
  throw new Error(msg);
}
  1. 类型收窄后出现“不可能的情况”
function assertNever(x: never): never {
  throw new Error("Unexpected value: " + x);
}

配合基础类型保护写 exhaustiveness check:

type Shape = 'circle' | 'square';

function area(s: Shape) {
  switch (s) {
    case 'circle': return 1;
    case 'square': return 1;
    default: 
      return assertNever(s); // 编译期报错,保证分支覆盖完整
  }
}
  1. 联合类型排除成员
type Exclude<T, U> = T extends U ? never : T;

“never 用于严格的编译期检查、不可达代码、以及保证联合类型逻辑完备性。”

🐯any

any 会关闭所有类型检查,是 TS 世界的“丧失类型安全”。适用于以下严格使用场景(只有这些情况能用)

  1. 渐进式迁移 JS → TS,需要暂时兜底
let temp: any = legacyLib.getData();
  1. 类型真的无法确定(第三方库,动态字段特别复杂)

比如老旧 SDK,或者大量动态 key。

  1. 需要与 JS 环境交互(特殊全局变量、动态属性)

例如 window 全局写入自定义字段。

  1. 为了避免过度复杂的类型推导(工程权衡)

例如复杂泛型导致 IDE 卡顿。

any 是一种工程妥协,不是类型,它是关闭类型检查的开关。只有在迁移、兼容、动态环境时才应该使用。

三者关系

  • unknown 是安全的顶类型(不信任输入 → 先 unknown)
  • never 是不可能发生的底类型(穷尽检查、异常、严格逻辑)
  • any 是逃生舱(完全关闭类型检查,只在必要时使用)

unknown 与 any 是对立的:一个提高安全,一个降低安全;

never 则代表类型系统的底部,是逻辑完备性检查的核心工具。

Partial & Pick & infer

Partial 和 Pick 是典型的映射类型,用来‘改造’对象键;

infer 是在条件类型中做‘反向推断’,固定在提取某些类型信息的场景里。

这三个是 TypeScript 类型系统里最基础也最常用的构建块,理解它们能写出真正类型安全的库级代码。”

🍒Partial

Partial<T> 会把类型 T 的所有属性变成可选属性。

它是一个典型的「映射类型」,通过遍历 keyof T,把每个键的属性加上 ? 修饰符。

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

工程使用场景:

  • 表单场景:编辑时部分字段可选
  • DTO/Object patch:只更新传入字段
  • 配置对象:允许用户覆盖默认配置

生产里基本把所有配置类型都写成 Partial 形式,减少冗余类型声明,也避免用户必须传所有属性。

🍑Pick

Pick<T, K> 从类型 T 中挑选部分键 K,生成一个子类型。

它同样是映射类型,只是遍历 K 而不是 keyof T

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

工程使用场景

  • API 拆分:只需要某些字段
  • React Props 提取
  • 二次封装组件时抽取部分属性

Pick 本质上就是结构化编程里的投影操作,非常常用。

🍇infer

infer 是 TypeScript 在条件类型里用于声明一个待推断的类型变量
它让 TypeScript 能“从类型中反向提取信息”。

  • 只能在条件类型 T extends X ? ... : ...true 分支里使用
  • 相当于告诉 TS:“这里有个未知类型 R,你帮我推断它是什么”
  • 最典型的例子:从函数类型中提取返回值

手写 ReturnType

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

常见 infer 使用:

  • 提取 Promise 内部类型
type Awaited<T> = T extends Promise<infer R> ? R : T;
  • 提取数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : T;

工程使用场景

  • 自动根据 API 类型生成 Response 类型
  • 根据函数库自动推断返回值
  • 在前端状态管理中自动生成 Action 类型
  • 在复杂表单里从 DTO 推类型

infer 是 TypeScript 高阶类型里很核心的机制,它让 TypeScript 能像编译器一样进行模式匹配。

as 断言

简单来说,as断言就是告诉TS编译器:"我知道这个变量是什么类型,请相信我!"

// 基础语法as 类型
// 或者
<类型>值  // 不常用,避免和JSX混淆

🎯 使用场景

1️⃣ DOM元素操作(最常用!)

// 不这样写
const input = document.getElementById('input') 
input.value // ❌ TS报错:对象可能为null

// 正确姿势 ✅
const input = document.getElementById('input') as HTMLInputElement
input.value // 搞定!

2️⃣ 联合类型的窄化

interface Cat {
  meow: () => void
  name: string
}

interface Dog {
  bark: () => void
  age: number
}

function handlePet(pet: Cat | Dog) {
  (pet as Cat).meow() // 断言为Cat类型
}

3️⃣ 处理any类型

const response: any = fetchData()
const data = response as UserData // 明确告诉TS这是UserData

⚠️ 避坑指南!

❌ 错误的断言

const num: string = '123'
const bool = num as boolean 
// 报错!string和boolean互不兼容

❌ 双重断言

const str = 'hello'
const num = str as unknown as number // 可以,但危险!

当两个类型完全不同,但你确定数据类型时,可以先转unknown/any

💡 使用建议

🔥 对比类型守卫:联合类型下优先采用类型守卫而不是断言!

// 不推荐:滥用as
function process(val: string | number) {
  (val as string).toUpperCase() // 危险!
}

// 推荐:类型守卫 ✅
function process(val: string | number) {
  if (typeof val === 'string') {
    val.toUpperCase() // 类型已窄化
  }
}

🎨 与as const的区别

const str1 = 'hello'  // 类型:string
const str2 = 'hello' as const  // 类型:'hello' 字面量

const arr1 = [1, 2]  // 类型:number[]
const arr2 = [1, 2] as const  // 类型:readonly [1, 2]
  • 1️⃣ 能不用的地方尽量不用 - 让TS自己推断
  • 2️⃣ 操作DOM时放心用 - 这是最安全的场景
  • 3️⃣ API返回数据谨慎用 - 最好配合校验
  • 4️⃣ 类型守卫优先 - 比as更安全

Q:"as断言会改变运行时类型吗?" A:不会! as只在编译时起作用,运行时类型由实际值决定

const data: any = 123
const str = data as string
console.log(typeof str) // 运行时还是 "number"!

as断言就像TS的"强制类型转换",用好了能救命,用不好会背锅!

类型谓词

类型谓词就是自定义类型守卫的返回值语法,格式:参数名 is 类型

function isFish(animal: Bird | Fish): animal is Fish {
  return (animal as Fish).swim !== undefined
}
//                    👆 这就是类型谓词!

基础语法解析

function 函数名(参数: 待检查的类型): 参数 is 目标类型 {
  // 返回boolean
  // true表示参数是目标类型
  // false表示参数不是目标类型
}

为什么需要类型谓词?

// ❌ 没有类型谓词的情况
function isFish(animal: Bird | Fish): boolean {
  return (animal as Fish).swim !== undefined
}

function move(animal: Bird | Fish) {
  if (isFish(animal)) {
    animal.swim() // ❌ 报错!虽然结果是true,但是TS还是不知道animal是Fish
  }
}

// ✅ 有了类型谓词
function isFish(animal: Bird | Fish): animal is Fish {
  return (animal as Fish).swim !== undefined
}

function move(animal: Bird | Fish) {
  if (isFish(animal)) {
    animal.swim() // ✅ 完美!TS知道是Fish
  }
}

🎨 类型谓词的各种妙用

1️⃣ 基础类型检查

// 检查是否为字符串
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

// 检查是否为数字
function isNumber(value: unknown): value is number {
  return typeof value === 'number' && !isNaN(value)
}

function process(value: unknown) {
  if (isString(value)) {
    return value.toUpperCase()  // string
  }
  if (isNumber(value)) {
    return value.toFixed(2)     // number
  }
}

2️⃣ 复杂对象验证

interface Article {
  id: number
  title: string
  content: string
  tags?: string[]
}

function isArticle(obj: any): obj is Article {
  return (
    obj !== null &&
    typeof obj === 'object' &&
    typeof obj.id === 'number' &&
    typeof obj.title === 'string' &&
    typeof obj.content === 'string' &&
    (obj.tags === undefined || 
     Array.isArray(obj.tags) && 
     obj.tags.every((tag: any) => typeof tag === 'string'))
  )
}

// 安全地使用API返回数据
async function getArticle(id: number) {
  const res = await fetch(`/api/articles/${id}`)
  const data = await res.json()
  
  if (isArticle(data)) {
    return data  // data被正确识别为Article
  }
  throw new Error('Invalid article data')
}

3️⃣ 数组类型守卫

// 检查数组中的每个元素
function isNumberArray(arr: any): arr is number[] {
  return Array.isArray(arr) && arr.every(item => typeof item === 'number')
}

function sum(values: unknown) {
  if (isNumberArray(values)) {
    return values.reduce((a, b) => a + b, 0)  // values是number[]
  }
  return 0
}

4️⃣ 联合类型窄化

type Success<T> = { status: 'success'; data: T }
type Failure = { status: 'error'; error: string }
type Result<T> = Success<T> | Failure

// 类型谓词判断成功状态
function isSuccess<T>(result: Result<T>): result is Success<T> {
  return result.status === 'success'
}

function handleResult<T>(result: Result<T>) {
  if (isSuccess(result)) {
    console.log(result.data)  // 正确识别为Success<T>
  } else {
    console.log(result.error) // 正确识别为Failure
  }
}

💡 类型谓词 vs 类型断言

// 类型断言 - 你自己保证
const data = response as User  // "相信我,这是User"

// 类型谓词 - 函数保证
function isUser(data: any): data is User {
  return data && typeof data.name === 'string'
}

if (isUser(response)) {
  const data = response  // "验证过了,安全使用"
}
// 类型谓词 - 返回boolean
function isPositive(num: any): num is number {
  return typeof num === 'number' && num > 0
}

// 断言函数 - 抛出错误
function assertIsPositive(num: any): asserts num is number {
  if (typeof num !== 'number' || num <= 0) {
    throw new Error('Not a positive number!')
  }
}

function processValue(value: any) {
  // 类型谓词:安全分支
  if (isPositive(value)) {
    return Math.sqrt(value)  // value是正数
  }
  
  // 断言函数:直接使用(确信不会错)
  assertIsPositive(value)
  return Math.sqrt(value)  // value也是正数
}

🚀 高级技巧:多参数类型谓词

// 检查两个值是否类型兼容
function areStrings(a: unknown, b: unknown): a is string {
  return typeof a === 'string' && typeof b === 'string'
}

function concatenate(a: unknown, b: unknown) {
  if (areStrings(a, b)) {
    return a + b  // a和b都被识别为string
  }
  return null
}

Q:如何实现一个可以检查对象是否包含特定属性的类型谓词?

// 进阶:泛型类型谓词
function hasProperty<T extends object, K extends PropertyKey>(
  obj: T, 
  prop: K
): obj is T & Record<K, unknown> {
  return prop in obj
}

interface User {
  name: string
  age?: number
}

function processUser(user: User) {
  if (hasProperty(user, 'age')) {
    console.log(user.age)  // 现在age一定存在!
  }
}

类型谓词的核心价值:

  1. 类型安全:运行时验证 + 编译时推导
  2. 代码复用:封装复杂的类型检查逻辑
  3. 可组合性:多个谓词可以组合使用

使用场景:

  • ✅ API响应数据验证
  • ✅ 表单输入验证
  • ✅ 复杂对象类型检查
  • ✅ 联合类型窄化
  • ❌ 简单的typeof检查(直接用typeof守卫)

类型守卫

类型守卫就是在运行时检查类型,让TS在特定作用域内推断出更精确的类型!

function process(value: string | number) {
  if (typeof value === 'string') {
    // 这里value自动推断为string类型
    return value.toUpperCase()
  }
  // 这里value自动推断为number类型
  return value.toFixed(2)
}

🎯 4种类型守卫写法

1️⃣ typeof守卫(基础但常用)

function format(value: string | number | boolean) {
  if (typeof value === 'string') {
    return `"${value}"`  // TS知道这里是string
  }
  
  if (typeof value === 'number') {
    return value.toFixed(2)  // number类型
  }
  
  return value ? 'true' : 'false'  // boolean类型
}

⚠️ 注意:typeof只能识别基础类型:

  • stringnumberboolean
  • symbolbigint
  • undefinedfunction
  • 不能识别:数组、对象、null

2️⃣ instanceof守卫(面向对象必备)

class Admin {
  role: 'admin' = 'admin'
  permissions: string[] = []
}

class User {
  role: 'user' = 'user'
  visits: number = 0
}

function handleUser(user: Admin | User) {
  if (user instanceof Admin) {
    return user.permissions  // 识别为Admin
  }
  return user.visits  // 识别为User
}

3️⃣ in守卫(检查属性是否存在)

interface Bird {
  fly: () => void
  layEggs: () => void
}

interface Fish {
  swim: () => void
  layEggs: () => void
}

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    return animal.fly()  // Bird类型
  }
  return animal.swim()  // Fish类型
}

4️⃣ 自定义类型守卫(🌟)

// 定义类型守卫
function isFish(animal: Bird | Fish): animal is Fish {
  return (animal as Fish).swim !== undefined
}

function process(animal: Bird | Fish) {
  if (isFish(animal)) {
    animal.swim()  // ✅ TS识别为Fish
  } else {
    animal.fly()   // ✅ TS识别为Bird
  }
}

🔥 实战案例:复杂的类型守卫

📦 区分数组和对象

function processData(data: Record<string, any> | any[]) {
  // 自定义守卫判断数组
  if (Array.isArray(data)) {
    return data.map(item => item)  // 识别为数组
  }
  
  // 或者判断对象
  if (data && typeof data === 'object' && !Array.isArray(data)) {
    return Object.keys(data)  // 识别为对象
  }
}

🎨 区分不同类型的对象

interface Circle {
  kind: 'circle'
  radius: number
}

interface Rectangle {
  kind: 'rectangle'
  width: number
  height: number
}

type Shape = Circle | Rectangle

// 可辨识联合(Discriminated Union)- 最优雅的方式!
function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2  // Circle类型
    case 'rectangle':
      return shape.width * shape.height   // Rectangle类型
  }
}

💡 类型守卫 vs as断言

对比类型守卫as断言
安全性✅ 运行时检查❌ 编译时欺骗
代码提示✅ 完美支持✅ 完美支持
使用场景不确定类型时确定类型时
学习成本更低
推荐指数⭐⭐⭐⭐⭐⭐⭐

🌟 高级技巧:断言函数

// 断言函数 - 如果错误直接抛出异常
function assertIsNumber(value: any): asserts value is number {
  if (typeof value !== 'number') {
    throw new Error('Not a number!')
  }
}

function double(value: any) {
  assertIsNumber(value)  // 断言通过后,value就是number
  return value * 2  // ✅ 不用as也能正确推断
}

Q:如何实现一个类型守卫判断某个值是否是Promise?

function isPromise<T = any>(value: any): value is Promise<T> {
  return value && typeof value.then === 'function'
}

// 使用
if (isPromise(data)) {
  data.then(res => console.log(res))  // 识别为Promise
}

extends

extends在TS中有多种身份:

  • 类继承(面向对象)
  • 接口扩展(类型组合)
  • 泛型约束(类型限制)
  • 条件类型(类型三目运算)
// 1️⃣ 类继承
class Animal {
  eat() {}
}
class Dog extends Animal {
  bark() {}
}

// 2️⃣ 接口扩展
interface Person {
  name: string
}
interface Employee extends Person {
  salary: number
}

// 3️⃣ 泛型约束
function logLength<T extends { length: number }>(arg: T) {
  console.log(arg.length)
}

// 4️⃣ 条件类型
type IsArray<T> = T extends any[] ? true : false

🎯 四大应用场景详解

1️⃣ 类继承(OOP基础)

class Animal {
  constructor(public name: string) {}
  
  move(distance: number = 0) {
    console.log(`${this.name} moved ${distance}m`)
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name)  // 必须调用super()
  }
  
  bark() {
    console.log('Woof! Woof!')
  }
  
  // 重写方法
  move(distance: number = 5) {
    console.log('Dog is running...')
    super.move(distance)
  }
}

const dog = new Dog('Buddy', 'Golden')
dog.bark()  // Woof! Woof!
dog.move()  // Dog is running... Buddy moved 5m

2️⃣ 接口扩展(组合类型)

// 基础接口
interface BasicInfo {
  name: string
  age: number
}

// 扩展接口
interface Address {
  street: string
  city: string
}

// 多接口扩展
interface Person extends BasicInfo, Address {
  email: string
  phone?: string
}

// 使用示例
const person: Person = {
  name: 'Alice',
  age: 25,
  street: '123 Main St',
  city: 'Beijing',
  email: 'alice@example.com'
}

// 接口扩展类(高级用法)
class Animal {
  type: string = 'mammal'
}

interface Dog extends Animal {
  bark(): void
}

class Husky implements Dog {
  type: string = 'mammal'
  bark() { console.log('Woof!') }
}

3️⃣ 泛型约束

// 基本约束
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length
}

getLength('hello')      // ✅ string有length
getLength([1, 2, 3])    // ✅ array有length
// getLength(123)       // ❌ number没有length

// 约束必须有特定属性
interface HasName {
  name: string
}

function greet<T extends HasName>(entity: T) {
  console.log(`Hello, ${entity.name}!`)
}

greet({ name: 'Alice', age: 25 })  // ✅
// greet({ age: 25 })               // ❌ 缺少name

// 多重约束(用交叉类型)
function process<T extends HasName & { age: number }>(obj: T) {
  console.log(`${obj.name} is ${obj.age} years old`)
}

// 约束key的类型
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]  // 返回类型 T[K]
}

const user = { name: 'Alice', age: 25, email: 'alice@example.com' }
getProperty(user, 'name')  // ✅
// getProperty(user, 'gender')  // ❌ gender不是key

4️⃣ 条件类型(TypeScript 2.8+)

// 基础条件类型
type IsString<T> = T extends string ? true : false

type A = IsString<string>  // true
type B = IsString<number>  // false

// 分布式条件类型(重点!)
type ToArray<T> = T extends any ? T[] : never
type StrOrNumArr = ToArray<string | number>  // string[] | number[]

// 内置条件类型
type NonNullable<T> = T extends null | undefined ? never : T
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

// 实战:深读只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

interface User {
  name: string
  address: {
    city: string
    street: string
  }
}

type ReadonlyUser = DeepReadonly<User>
// 所有属性都变成readonly!

💡 extends vs 其他操作符

🔥 extends vs implements

interface Flyable {
  fly(): void
}

// extends:继承实现
class Bird {
  layEggs() {}
}

class Eagle extends Bird implements Flyable {
  fly() {}  // 必须实现Flyable
  // layEggs() 已经继承
}

// implements:只约束类型
class Airplane implements Flyable {
  fly() {}  // 只需要实现接口
  // 不需要继承任何东西
}

🔥 extends in type vs interface

// interface只能扩展对象类型
interface Person {
  name: string
}

interface Employee extends Person {
  salary: number
}

// type可以使用条件类型
type IsNumber<T> = T extends number ? true : false

// type可以扩展联合类型
type Animal = 'dog' | 'cat' | 'bird'
type Pet = Animal extends 'dog' ? 'good boy' : Animal

🚀 高级技巧:条件类型的实际应用

1️⃣ 排除特定类型

type Exclude<T, U> = T extends U ? never : T

type T1 = Exclude<'a' | 'b' | 'c', 'a'>  // 'b' | 'c'
type T2 = Exclude<string | number | boolean, number>  // string | boolean

2️⃣ 提取特定类型

type Extract<T, U> = T extends U ? T : never

type T3 = Extract<'a' | 'b' | 'c', 'a' | 'b'>  // 'a' | 'b'
type T4 = Extract<string | number | boolean, Function>  // never

3️⃣ 函数类型提取

// 提取函数参数类型
type FirstArgument<T> = T extends (arg: infer A, ...args: any[]) => any ? A : never

type Fn = (name: string, age: number) => void
type Arg = FirstArgument<Fn>  // string

// 提取Promise返回值
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T

type PromiseType = UnwrapPromise<Promise<string>>  // string
type NormalType = UnwrapPromise<number>  // number

Q1:extends在条件类型中的分布式行为是什么意思?

// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never

// 当T是联合类型时,会分别计算每个类型
type Result = ToArray<string | number>  
// 等价于:string[] | number[]
// 而不是:(string | number)[]

// 如果想禁止分布式,用方括号包起来
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result2 = ToArrayNonDist<string | number>  // (string | number)[]

Q2:如何实现Pick类型?

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

interface User {
  name: string
  age: number
  email: string
}

type UserNameAndAge = MyPick<User, 'name' | 'age'>
// 结果:{ name: string; age: number; }

Q3:如何实现递归部分可选?

type PartialDeep<T> = T extends object ? {
  [P in keyof T]?: PartialDeep<T[P]>
} : T

interface User {
  name: string
  address: {
    city: string
    street: string
  }
}

type PartialUser = PartialDeep<User>
// 所有属性都变成可选,包括嵌套的!

组合技巧:

  • extends + keyof = 安全的属性访问
  • extends + infer = 类型提取
  • extends + 联合类型 = 分布式条件类型

函数重载

函数重载是指同一个函数名,不同参数类型或数量的多个定义

// 函数重载的写法
function greet(name: string): string
function greet(age: number): string
function greet(value: string | number): string {
  if (typeof value === 'string') {
    return `Hello, ${value}!`
  }
  return `You are ${value} years old`
}

greet('Alice')  // ✅ Hello, Alice!
greet(25)       // ✅ You are 25 years old

🎯 函数重载的两种形式

1️⃣ 基础重载(多参数组合)

// 重载签名
function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a: any, b: any): any {
  return a + b
}

add(1, 2)        // ✅ 3
add('1', '2')    // ✅ '12'
// add(1, '2')   // ❌ 没有匹配的重载

2️⃣ 可选参数重载

// 重载签名
function format(value: string): string
function format(value: string, uppercase: boolean): string
function format(value: string, uppercase?: boolean): string {
  return uppercase ? value.toUpperCase() : value
}

format('hello')           // ✅ 'hello'
format('hello', true)     // ✅ 'HELLO'

🔥 函数重载 vs 联合类型

// 方式1:联合类型(简单场景)
function double(value: string | number): string | number {
  if (typeof value === 'string') {
    return value + value
  }
  return value * 2
}

const result = double('hi')  // 类型:string | number ❌ 不精确

// 方式2:函数重载(精确类型)
function double(value: string): string
function double(value: number): number
function double(value: string | number): string | number {
  if (typeof value === 'string') {
    return value + value
  }
  return value * 2
}

const str = double('hi')  // 类型:string ✅ 精确!
const num = double(5)     // 类型:number ✅ 精确!

🆚 函数重载 vs 泛型

// 泛型:类型参数化
function identity<T>(arg: T): T {
  return arg
}

identity<string>('hello')  // 返回string
identity<number>(123)      // 返回number
维度函数重载泛型
适用场景固定几种类型组合类型之间有关联
灵活性低(要列出所有情况)高(任意类型)
精确度极高(可指定返回类型)高(保持关系)
维护成本高(类型多了难维护)低(一个搞定)
// 场景1:不同类型不同逻辑(适合重载)
function process(input: string): string[]
function process(input: number): number
function process(input: string | number): string[] | number {
  if (typeof input === 'string') {
    return input.split('')
  }
  return input * 2
}

// 场景2:类型关系保持一致(适合泛型)
function wrapInArray<T>(value: T): T[] {
  return [value]
}

wrapInArray('hello')  // string[]
wrapInArray(123)      // number[]

💡 实现一个reverse函数

// 需求:输入字符串返回字符串,输入数组返回数组
// 解法1:函数重载
function reverse(value: string): string
function reverse<T>(value: T[]): T[]
function reverse(value: string | any[]): string | any[] {
  if (typeof value === 'string') {
    return value.split('').reverse().join('')
  }
  return value.reverse()
}

// 解法2:泛型(更优雅!)
function reverse<T extends string | any[]>(value: T): T {
  if (typeof value === 'string') {
    return value.split('').reverse().join('') as T
  }
  return value.reverse() as T
}

// 解法3:条件类型(最精确!)
type ReverseReturnType<T> = T extends string ? string : T extends any[] ? T : never

function reverse<T extends string | any[]>(value: T): ReverseReturnType<T> {
  if (typeof value === 'string') {
    return value.split('').reverse().join('') as ReverseReturnType<T>
  }
  return value.reverse() as ReverseReturnType<T>
}

Q1:函数重载的解析顺序是怎样的?

function foo(x: string): string
function foo(x: number): number
function foo(x: any): any {
  return x
}

// TS会按顺序匹配第一个匹配的重载
foo('hello')  // 匹配第一个
foo(123)      // 匹配第二个
foo(true)     // ❌ 没有匹配,报错

Q2:如何实现一个可以接受不同参数个数的函数?

// 需求:sum(1,2) => 3; sum(1,2,3) => 6
// 解法:使用rest参数 + 重载
function sum(a: number, b: number): number
function sum(a: number, b: number, c: number): number
function sum(...args: number[]): number {
  return args.reduce((acc, curr) => acc + curr, 0)
}

sum(1, 2)     // ✅ 3
sum(1, 2, 3)  // ✅ 6
// sum(1)     // ❌ 没有匹配

Q3:重载和可选参数的区别?

// 可选参数版本
function greet(name: string, age?: number): string {
  return age ? `${name} is ${age}` : `Hello ${name}`
}

// 重载版本
function greet(name: string): string
function greet(name: string, age: number): string
function greet(name: string, age?: number): string {
  return age ? `${name} is ${age}` : `Hello ${name}`
}

// 区别:重载可以定义不同的返回类型!
function process(input: string): string[]
function process(input: number): number
// 可选参数做不到返回类型不同

函数重载的核心价值:

  1. 精确类型:不同参数返回不同类型
  2. 类型安全:限制非法参数组合
  3. 文档作用:清晰展示函数用法