进阶之路:类型工具与声明文件

目录

第 1 节 概述

前几篇我们学会了 TypeScript 的日常用法:基础类型、接口、函数、类、泛型、模块。

但 TypeScript 的类型系统远比这强大——它实际上是一门独立的编程语言。你可以对类型做运算、条件判断、循环遍历、字符串拼接……

本篇的内容偏"进阶"。理解它们需要一段时间,但一旦掌握,你就能写出更灵活、更安全的类型定义。

第 2 节 映射类型

映射类型可以基于一个类型,生成另一个类型。它的语法像循环:

1
2
type T = { [K in "a" | "b" | "c"]: boolean }
// 等价于 { a: boolean; b: boolean; c: boolean }

真实场景——把一个接口的所有属性变为可选:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface Person {
  name: string
  age: number
  email: string
}

// 手动写一份可选版本
type PartialPerson = {
  name?: string
  age?: number
  email?: string
}

用映射类型自动生成:

1
2
3
4
5
6
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

type PartialPerson = MyPartial<Person>
// { name?: string; age?: number; email?: string }

在映射类型中可以加修饰符:? 表示可选,readonly 表示只读。前缀 - 表示去除某个修饰符:

1
2
3
4
5
6
7
8
9
// 去除所有 readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
}

// 去除所有可选标记
type Required<T> = {
  [K in keyof T]-?: T[K]
}

第 3 节 条件类型

条件类型为类型添加了"if-else"逻辑:

1
2
3
4
type IsString<T> = T extends string ? "是字符串" : "不是字符串"

type A = IsString<"hello">   // "是字符串"
type B = IsString<42>        // "不是字符串"
理解

条件类型类似于三元运算符:T extends U ? X : Y。如果 T 能赋值给 U,结果为 X,否则为 Y。

实际用途——根据参数类型决定返回值类型:

1
2
3
4
5
6
type ApiResponse<T> = T extends "user" ? { id: number; name: string }
  : T extends "post" ? { id: number; title: string }
  : { error: string }

type UserRes = ApiResponse<"user">  // { id: number; name: string }
type PostRes = ApiResponse<"post">  // { id: number; title: string }

3.1 条件类型中的 infer

infer 可以在条件类型中声明一个"待推断"的类型变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 获取数组元素的类型
type ElementType<T> = T extends (infer E)[] ? E : never

type A = ElementType<string[]>  // string
type B = ElementType<number[]>  // number
type C = ElementType<boolean>   // never(不是数组)

// 获取函数返回值的类型
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never

type Fn = (x: number) => string
type R = ReturnType<Fn>  // string

infer 是高级类型编程中最强大的工具之一。它让 TypeScript 帮我们"反向推导"出类型。

第 4 节 模板字面量类型

TypeScript 4.1 引入了模板字面量类型——就像 JavaScript 的模板字符串,但在类型层面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type EventName = `on${Capitalize<string>}`

// 组合字符串字面量
type Size = "small" | "medium" | "large"
type Color = "red" | "green" | "blue"

type ButtonVariant = `${Size}-${Color}`
// "small-red" | "small-green" | "small-blue"
// | "medium-red" | "medium-green" | "medium-blue"
// | "large-red" | "large-green" | "large-blue"

实用的例子——约束事件处理函数的命名:

1
2
3
4
type EventHandler<K extends string> = `on${Capitalize<K>}`

type ClickHandler = EventHandler<"click">  // "onClick"
type ChangeHandler = EventHandler<"change"> // "onChange"

第 5 节 内置工具类型

TypeScript 内置了一套常用的泛型工具类型,可以让你的类型编程事半功倍:

5.1 对象操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface User {
  name: string
  age: number
  email: string
  password: string
}

// Partial<T> —— 所有属性变为可选
type PartialUser = Partial<User>
// { name?: string; age?: number; email?: string; password?: string }

// Required<T> —— 所有属性变为必选
type RequiredUser = Required<PartialUser>
// { name: string; age: number; email: string; password: string }

// Readonly<T> —— 所有属性变为只读
type ReadonlyUser = Readonly<User>

// Pick<T, K> —— 挑选部分属性
type PublicUser = Pick<User, "name" | "email">
// { name: string; email: string }

// Omit<T, K> —— 排除部分属性
type InternalUser = Omit<User, "password">
// { name: string; age: number; email: string }

5.2 联合类型操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Status = "idle" | "loading" | "success" | "error"

// Exclude<T, U> —— 排除某些类型
type NoLoading = Exclude<Status, "loading">
// "idle" | "success" | "error"

// Extract<T, U> —— 提取某些类型
type LoadingStates = Extract<Status, "loading" | "error">
// "loading" | "error"

// NonNullable<T> —— 去除 null 和 undefined
type Maybe = string | null | undefined
type Sure = NonNullable<Maybe>  // string

5.3 记录类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Record<K, V> —— 构造键值对类型
type PageRoutes = Record<string, string>
const routes: PageRoutes = {
  home: "/",
  about: "/about",
}

// 更精确的用法——限制键
type PageName = "home" | "about" | "contact"
type PageInfo = Record<PageName, { title: string; path: string }>

const pages: PageInfo = {
  home: { title: "首页", path: "/" },
  about: { title: "关于", path: "/about" },
  contact: { title: "联系", path: "/contact" },
}

5.4 其他常用工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Awaited<T> —— 获取 Promise 内部类型
type AsyncResult = Awaited<Promise<string>>  // string
type DeepAsync = Awaited<Promise<Promise<number>>>  // number

// Parameters<T> —— 获取函数参数类型(元组)
type Fn = (name: string, age: number) => void
type FnParams = Parameters<Fn>  // [string, number]

// ConstructorParameters<T> —— 获取构造参数类型
class Person { constructor(name: string, age: number) {} }
type PersonParams = ConstructorParameters<typeof Person>  // [string, number]

// ReturnType<T> —— 获取函数返回值类型
type FnReturn = ReturnType<() => string>  // string

// InstanceType<T> —— 获取类实例的类型
type PersonInstance = InstanceType<typeof Person>  // Person

第 6 节 声明文件(.d.ts)

当你在 TypeScript 中使用 JavaScript 库(如 lodash、jQuery)时,需要类型声明文件来告诉 TypeScript 这些函数的类型。

1
2
3
// lodash 的简化声明
declare function chunk<T>(array: T[], size?: number): T[][]
declare function shuffle<T>(array: T[]): T[]

6.1 声明文件的来源

  • 自带声明:库的 package.json 中有 "types": "index.d.ts",如 Vue
  • DefinitelyTypednpm install @types/lodash,社区维护的声明
  • 自己写:在项目中创建 .d.ts 文件

6.2 declare 关键字

declare 告诉 TypeScript"这个变量存在,但实现在别处":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 声明全局变量
declare const API_VERSION: string

// 声明全局函数
declare function fetchData(url: string): Promise<unknown>

// 声明模块
declare module "my-lib" {
  export function doSomething(): void
  export const VERSION: string
}

// 声明类
declare class MyClass {
  constructor(name: string)
  greet(): string
}

6.3 环境配置

.d.ts 文件中声明全局类型,整个项目都可以使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// env.d.ts —— 放在项目 src 目录下
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: "development" | "production" | "test"
    API_URL: string
  }
}

// 使用
const env = process.env.NODE_ENV  // 类型为 "development" | "production" | "test"
注意

声明文件(.d.ts)只包含类型信息,不包含实现。它就像一份"说明书",告诉 TypeScript 外部世界的类型。编译时不会生成对应的 JS 代码。


进阶之路到这里就告一段落了。你接触了 TypeScript 类型系统的"编程"能力——映射、条件、推断、模板字面量、工具类型。

好的,理论足够多了。下一篇是真正的收尾——用 TypeScript 从头到尾做一个完整项目。

目录