Skip to content

TypeScript 基础知识到类型体操

文章基于 v5.8.3 版本

Typescript 的好处:

  • 提前在编辑器知道变量的类型,而不用去打印才知道

  • 类型安全,减少运行时错误,比如有个值是 undefined,没有 TS 可能导致线上报错花不少时间才找到问题

编辑器 typescript 配置提示

配置后,编辑器会自动提示变量的类型,方便及时发现问题

typescript inlay hints

页面截图 - 提示变量类型

  • vscode 插件 typescript imports 可以自动导入模块
  • ts-node 可以运行 ts 文件
  • ts-node-dev 可以热更新 ts 文件

tsconfig.json 配置

tsconfig

下面是当前博客 Nextjs 项目的 tsconfig.json 配置,我查阅了文档,给配置增加了注释,方便理解 TS 在项目中的使用

json
{
  "compilerOptions": {
    // 编译选项
    "target": "ES2017", // 目标版本
    "lib": ["dom", "dom.iterable", "esnext"], // 内置类型,配置下就能用
    "allowJs": true, // 允许在项目中导入 JavaScript 文件,而不仅仅是 .ts 和 .tsx 文件。例如,此 JS 文件:
    "skipLibCheck": true, // 跳过库文件的类型检查
    "strict": true, // 开启所有严格检查,默认 strictNullChecks noImplicitAny
    "noEmit": true, // 不编译出文件
    "esModuleInterop": true, // https://www.typescriptlang.org/tsconfig/#esModuleInterop
    "module": "esnext", // 模块
    "moduleResolution": "bundler", // 模块解析策略
    "resolveJsonModule": true, // 允许导入 JSON 模块
    "isolatedModules": true, // 每个文件被视为一个模块
    "jsx": "preserve", // 保留 JSX 语法
    "incremental": true, // 增量编译
    "plugins": [
      // 插件
      {
        "name": "next"
      }
    ],
    "paths": {
      // 路径别名
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], // 包含的文件
  "exclude": ["node_modules"] // 排除的文件
}
{
  "compilerOptions": {
    // 编译选项
    "target": "ES2017", // 目标版本
    "lib": ["dom", "dom.iterable", "esnext"], // 内置类型,配置下就能用
    "allowJs": true, // 允许在项目中导入 JavaScript 文件,而不仅仅是 .ts 和 .tsx 文件。例如,此 JS 文件:
    "skipLibCheck": true, // 跳过库文件的类型检查
    "strict": true, // 开启所有严格检查,默认 strictNullChecks noImplicitAny
    "noEmit": true, // 不编译出文件
    "esModuleInterop": true, // https://www.typescriptlang.org/tsconfig/#esModuleInterop
    "module": "esnext", // 模块
    "moduleResolution": "bundler", // 模块解析策略
    "resolveJsonModule": true, // 允许导入 JSON 模块
    "isolatedModules": true, // 每个文件被视为一个模块
    "jsx": "preserve", // 保留 JSX 语法
    "incremental": true, // 增量编译
    "plugins": [
      // 插件
      {
        "name": "next"
      }
    ],
    "paths": {
      // 路径别名
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], // 包含的文件
  "exclude": ["node_modules"] // 排除的文件
}

type 和 interface 的区别

type 和 interface 的区别

type 是别名不能扩展,interface 是可扩展的,interface + extends 性能更好,也就是用来继承

1、在 TypeScript 版本 4.2 之前,type 可能会出现在错误消息中 ,有时会代替等效的匿名类型(这可能是可取的,也可能不可取的)。interface 将始终在错误消息中命名。

2、type 不能参与声明合并,但 interface 可以 。

ts
interface Mammal {
  genus: string
}

interface Mammal {
  breed?: string
}

const animal: Mammal = {
  genus: '1234',
  breed: 1
}

type Reptile = {
  genus: string
}

// 不能这样 ❌
type Reptile = {
  breed?: string
}
interface Mammal {
  genus: string
}

interface Mammal {
  breed?: string
}

const animal: Mammal = {
  genus: '1234',
  breed: 1
}

type Reptile = {
  genus: string
}

// 不能这样 ❌
type Reptile = {
  breed?: string
}

3、接口只能用于声明对象的形状,而不能用于重命名基元 。

ts
// Interface 方式
interface AnObject1 {
  value: string
}

// Type 方式
type AnObject2 = {
  value: string
}

// 两种方式都可以用来描述对象的结构,功能基本相同。

// ✅ type 可以为现有的基础类型创建别名
type SanitizedString = string
type EvenNumber = number

// ❌ interface 不能这样做
interface X extends string {
  // 这会报错!interface 不能继承基础类型
}
// Interface 方式
interface AnObject1 {
  value: string
}

// Type 方式
type AnObject2 = {
  value: string
}

// 两种方式都可以用来描述对象的结构,功能基本相同。

// ✅ type 可以为现有的基础类型创建别名
type SanitizedString = string
type EvenNumber = number

// ❌ interface 不能这样做
interface X extends string {
  // 这会报错!interface 不能继承基础类型
}

4、interface 将始终以其原始形式显示在错误消息中,但前提是它们按 name 使用。

ts
interface Mammal {
  name: string
}

function echoMammal(m: Mammal) {
  console.log(m.name)
}

// 错误信息会明确提到 "Mammal" 这个接口名称
echoMammal({ name: 12343 })

// m 的类型和上面的 Mammal 完全相同,但没有被直接命名
function echoAnimal(m: { name: string }) {
  console.log(m.name)
}

echoAnimal({ name: 12345 })

// 错误信息类似:
// 类型 'number' 不能赋值给类型 'string'。
// 参数类型 '{ name: number }' 不能赋值给参数类型 '{ name: string }'。
// 当对象类型很复杂时,这个区别就非常明显
interface Mammal {
  name: string
}

function echoMammal(m: Mammal) {
  console.log(m.name)
}

// 错误信息会明确提到 "Mammal" 这个接口名称
echoMammal({ name: 12343 })

// m 的类型和上面的 Mammal 完全相同,但没有被直接命名
function echoAnimal(m: { name: string }) {
  console.log(m.name)
}

echoAnimal({ name: 12345 })

// 错误信息类似:
// 类型 'number' 不能赋值给类型 'string'。
// 参数类型 '{ name: number }' 不能赋值给参数类型 '{ name: string }'。
// 当对象类型很复杂时,这个区别就非常明显

5、对于编译器来说,使用带有 extends 的接口通常比使用带有交集的类型别名性能更高

preferring-interfaces-over-intersections

大概是说交集的 type 会递归合并,而 interface 不会只会 flat,然后 interface 会缓存

类型断言

如使用 document.getElementById 的时候,TypeScript 只知道这将返回某种 HTMLElement,但你可能知道你的页面将始终具有具有给定 ID 的 HTMLCanvasElement

ts
const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement
const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement

object、Object 以及

  • Object 包含了所有的类型,避免使用
  • object 表示所有非原始类型的类型,即数组、对象与函数类型
  • {} 表示空对象类型,任何非 null / undefined 的值,不可赋值,避免使用

泛型

泛型允许我们在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型。这样可以让我们写出更加通用和类型安全的代码。

泛型约束 🔥

使用 extends 关键字来约束泛型类型:

ts
// 约束 T 必须有 length 属性
interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // 现在我们知道 arg 有 length 属性
  return arg
}

// ✅ 正确使用
loggingIdentity('hello') // string 有 length
loggingIdentity([1, 2, 3]) // array 有 length

// ❌ 错误使用
// loggingIdentity(3) // number 没有 length 属性
// 约束 T 必须有 length 属性
interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // 现在我们知道 arg 有 length 属性
  return arg
}

// ✅ 正确使用
loggingIdentity('hello') // string 有 length
loggingIdentity([1, 2, 3]) // array 有 length

// ❌ 错误使用
// loggingIdentity(3) // number 没有 length 属性

映射类型与泛型

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

// 将所有属性变为只读
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 将所有属性变为可选
type MyPartial<T> = {
  [P in keyof T]?: T[P]
}

// 将数组中的每个元素转换为对象的键
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P
}

interface User {
  name: string
  age: number
}

type PartialUser = MyPartial<User> // { name?: string; age?: number }
type ReadonlyUser = MyReadonly<User> // { readonly name: string; readonly age: number }
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// 将所有属性变为只读
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 将所有属性变为可选
type MyPartial<T> = {
  [P in keyof T]?: T[P]
}

// 将数组中的每个元素转换为对象的键
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P
}

interface User {
  name: string
  age: number
}

type PartialUser = MyPartial<User> // { name?: string; age?: number }
type ReadonlyUser = MyReadonly<User> // { readonly name: string; readonly age: number }

实际应用场景

ts
// API 响应类型
interface ApiResponse<T> {
  data: T
  success: boolean
  message: string
}

// 使用
type UserResponse = ApiResponse<User>
type UsersResponse = ApiResponse<User[]>

// 异步函数返回类型
async function fetchUser(id: string): Promise<ApiResponse<User>> {
  // 实现...
}

// 异步函数返回类型
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  // 实现...
}
// API 响应类型
interface ApiResponse<T> {
  data: T
  success: boolean
  message: string
}

// 使用
type UserResponse = ApiResponse<User>
type UsersResponse = ApiResponse<User[]>

// 异步函数返回类型
async function fetchUser(id: string): Promise<ApiResponse<User>> {
  // 实现...
}

// 异步函数返回类型
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  // 实现...
}

函数重载

函数重载允许为同一个函数提供多个类型签名,让 TypeScript 能够根据参数类型推断出正确的返回类型。

基本语法

ts
// 重载签名(只有类型声明)
function process(value: string): string
function process(value: number): number
function process(value: boolean): string

// 实现签名(具体实现)
function process(value: string | number | boolean): string | number {
  if (typeof value === 'string') {
    return value.toUpperCase()
  }
  if (typeof value === 'number') {
    return value * 2
  }
  return value.toString()
}

// 使用时 TypeScript 会根据参数类型推断返回类型
const result1 = process('hello') // string
const result2 = process(42) // number
const result3 = process(true) // string
// 重载签名(只有类型声明)
function process(value: string): string
function process(value: number): number
function process(value: boolean): string

// 实现签名(具体实现)
function process(value: string | number | boolean): string | number {
  if (typeof value === 'string') {
    return value.toUpperCase()
  }
  if (typeof value === 'number') {
    return value * 2
  }
  return value.toString()
}

// 使用时 TypeScript 会根据参数类型推断返回类型
const result1 = process('hello') // string
const result2 = process(42) // number
const result3 = process(true) // string

实际应用场景

ts
// DOM 元素获取
function getElement(selector: string): HTMLElement | null
function getElement(element: HTMLElement): HTMLElement
function getElement(input: string | HTMLElement): HTMLElement | null {
  if (typeof input === 'string') {
    return document.querySelector(input)
  }
  return input
}
// DOM 元素获取
function getElement(selector: string): HTMLElement | null
function getElement(element: HTMLElement): HTMLElement
function getElement(input: string | HTMLElement): HTMLElement | null {
  if (typeof input === 'string') {
    return document.querySelector(input)
  }
  return input
}

内置工具类型

内置工具类型

ts
// 返回 Promise 的值类型
Awaited<T>

// 可选
Partial<T>

// 必选
Required<T>

// 只读
Readonly<T>

// 将 K 中的每个属性值映射到 V,返回一个对象类型,可以使用 Record 类型来声明属性名还未确定的接口类型
Record<K, V>

// 从 T 中选择一组属性 K,返回一个新类型
Pick<T, K>

// 从 T 中排除一组属性 K,返回一个新类型
Omit<T, K>

// 从 UnionType T 中排除 U 中的类型,T是联合类型,返回一个新类型
Exclude<T, U>

// 从 UnionType T 中提取 U 中的类型,T是联合类型,返回一个新类型
Extract<T, U>

// 返回函数参数的类型
Parameters<T>

// 返回函数返回值的类型
ReturnType<T>
// 返回 Promise 的值类型
Awaited<T>

// 可选
Partial<T>

// 必选
Required<T>

// 只读
Readonly<T>

// 将 K 中的每个属性值映射到 V,返回一个对象类型,可以使用 Record 类型来声明属性名还未确定的接口类型
Record<K, V>

// 从 T 中选择一组属性 K,返回一个新类型
Pick<T, K>

// 从 T 中排除一组属性 K,返回一个新类型
Omit<T, K>

// 从 UnionType T 中排除 U 中的类型,T是联合类型,返回一个新类型
Exclude<T, U>

// 从 UnionType T 中提取 U 中的类型,T是联合类型,返回一个新类型
Extract<T, U>

// 返回函数参数的类型
Parameters<T>

// 返回函数返回值的类型
ReturnType<T>

Pick 与 Omit 类型是类型编程中相当重要的一个部分,举例来说,我们可以先声明一个代表全局所有状态的大型接口类型:

ts
type User = {
  name: string
  age: number
  email: string
  phone: string
  address: string
  gender: string
  occupation: string
  education: string
  hobby: string
  bio: string
}
type User = {
  name: string
  age: number
  email: string
  phone: string
  address: string
  gender: string
  occupation: string
  education: string
  hobby: string
  bio: string
}

然后在我们的子组件中,可能只用到了其中一部分的类型,此时就可以使用 Pick 类型将我们需要的部分择出来:

ts
type UserBasicInfo = Pick<User, 'name' | 'age' | 'email'>
type UserBasicInfo = Pick<User, 'name' | 'age' | 'email'>

反之,如果我们用到了大部分类型,只有数个类型需要移除,就可以使用 Omit 类型来减少一些代码量:

ts
type UserDetailedInfo = Omit<User, 'name' | 'age' | 'email'>
type UserDetailedInfo = Omit<User, 'name' | 'age' | 'email'>

模版字符串类型

此前使用字面量类型来提供精确的类型定义时,会面临的一个问题就是当可用的字面量类型过多,自己一个个写会非常的头痛,可能就直接选择用 string 类型了。而现在,假设这些字面量类型满足一定的规律,我们就可以通过模板字符串类型的自动分发特性,来实现由排列组合自动生成联合类型了!

ts
type Version = `${number}.${number}.${number}`

const v1: Version = '1.1.0'
const v2: Version = '1.0' // 报错:类型 "1.0" 不能赋值给类型 `${number}.${number}.${number}`
const v3: Version = 'a.0.0' // 报错:类型 "a.0" 不能赋值给类型 `${number}.${number}.${number}`
type Version = `${number}.${number}.${number}`

const v1: Version = '1.1.0'
const v2: Version = '1.0' // 报错:类型 "1.0" 不能赋值给类型 `${number}.${number}.${number}`
const v3: Version = 'a.0.0' // 报错:类型 "a.0" 不能赋值给类型 `${number}.${number}.${number}`
ts
type Brand = 'iphone' | 'xiaomi' | 'honor'

type SKU = `${Brand}` // "iphone" | "xiaomi" | "honor"

type SKU = `${Brand}-latest` // "iphone-latest" | "xiaomi-latest" | "honor-latest"

type Brand2 = 'iphone' | 'xiaomi' | 'honor'
type Memory = '16G' | '64G'
type ItemType = 'official' | 'second-hand'

type SKU2 = `${Brand2}-${Memory}-${ItemType}` // 多种排列组合
type Brand = 'iphone' | 'xiaomi' | 'honor'

type SKU = `${Brand}` // "iphone" | "xiaomi" | "honor"

type SKU = `${Brand}-latest` // "iphone-latest" | "xiaomi-latest" | "honor-latest"

type Brand2 = 'iphone' | 'xiaomi' | 'honor'
type Memory = '16G' | '64G'
type ItemType = 'official' | 'second-hand'

type SKU2 = `${Brand2}-${Memory}-${ItemType}` // 多种排列组合

infer

infer 关键字用于在条件类型中推断类型,可以从复杂类型中提取我们需要的部分。

基本语法

ts
// 基本模式:T extends SomeType<infer U> ? U : never
type GetArrayType<T> = T extends (infer U)[] ? U : never

type StringArray = GetArrayType<string[]> // string
type NumberArray = GetArrayType<number[]> // number
type NotArray = GetArrayType<string> // never

// 提取第一个数组第一个类型
type First<T extends unknown[]> = T extends [infer A, ...unknown[]] ? A : never
// 基本模式:T extends SomeType<infer U> ? U : never
type GetArrayType<T> = T extends (infer U)[] ? U : never

type StringArray = GetArrayType<string[]> // string
type NumberArray = GetArrayType<number[]> // number
type NotArray = GetArrayType<string> // never

// 提取第一个数组第一个类型
type First<T extends unknown[]> = T extends [infer A, ...unknown[]] ? A : never

逆变和协变

协变,子类型可以赋值给父类型

ts
interface Person {
  name: string
}

interface Son {
  name: string
  hobbies: string[]
}

let person: Person = {
  name: ''
}

let son: Son = {
  name: '111',
  hobbies: ['play game', 'writing']
}

person = son // ✅

son = person // ❌
interface Person {
  name: string
}

interface Son {
  name: string
  hobbies: string[]
}

let person: Person = {
  name: ''
}

let son: Son = {
  name: '111',
  hobbies: ['play game', 'writing']
}

person = son // ✅

son = person // ❌

函数的参数有逆变的性质,而返回值是协变的,也就是返回值的子类型可以赋值给父类型

ts
interface Person {
  name: string
}

interface Son {
  name: string
  hobbies: string[]
}

let printHobbies: (son: Son) => void

printHobbies = (son) => {
  console.log(son.hobbies)
}

let printName: (person: Person) => void

printName = (person) => {
  console.log(person.name)
}

// 父类型参数赋值给子
printHobbies = printName // ✅

printName = printHobbies // ❌
interface Person {
  name: string
}

interface Son {
  name: string
  hobbies: string[]
}

let printHobbies: (son: Son) => void

printHobbies = (son) => {
  console.log(son.hobbies)
}

let printName: (person: Person) => void

printName = (person) => {
  console.log(person.name)
}

// 父类型参数赋值给子
printHobbies = printName // ✅

printName = printHobbies // ❌

函数的参数有逆变(父类型赋值给子类型)的性质

  • 这是函数类型,函数的参数有逆变的特性,所以父类型赋值给子类型是允许的
  • 下面是函数参数 hello 子类型赋值给父类型所以报错
  • 返回值的位置是协变的,也就是赋值的函数的返回值是被赋值的函数的返回值的子类型
ts
// 前置只是 undefined 是 void子类型

type A = undefined
type B = void

type C = A extends B ? true : false // true
type D = B extends A ? true : false // false

//

type Func = (a: string) => void

// func 会报错
// 不能将类型“(a: "hello") => undefined”分配给类型“Func”。
//   参数“a”和“a” 的类型不兼容。
//     不能将类型“string”分配给类型“"hello"”
const func: Func = (a: 'hello') => undefined
// 前置只是 undefined 是 void子类型

type A = undefined
type B = void

type C = A extends B ? true : false // true
type D = B extends A ? true : false // false

//

type Func = (a: string) => void

// func 会报错
// 不能将类型“(a: "hello") => undefined”分配给类型“Func”。
//   参数“a”和“a” 的类型不兼容。
//     不能将类型“string”分配给类型“"hello"”
const func: Func = (a: 'hello') => undefined

手写内置工具类型

手写 Awaited

ts
type MyAwaited<T> = T extends Promise<infer Value> ? Value : never

type A = MyAwaited<Promise<string>>
type MyAwaited<T> = T extends Promise<infer Value> ? Value : never

type A = MyAwaited<Promise<string>>

手写 Partial

ts
type MyPartial<T> = {
  [Key in keyof T]?: T[Key]
}

interface Todo {
  title: string
  description: string
}

type A = MyPartial<Todo>
type MyPartial<T> = {
  [Key in keyof T]?: T[Key]
}

interface Todo {
  title: string
  description: string
}

type A = MyPartial<Todo>

手写 Required

ts
type MyRequired<T> = {
  [Key in keyof T]-?: T[Key]
}

interface Props {
  a?: number
  b?: string
}

// 报错
const obj2: MyRequired<Props> = { a: 5 }
type MyRequired<T> = {
  [Key in keyof T]-?: T[Key]
}

interface Props {
  a?: number
  b?: string
}

// 报错
const obj2: MyRequired<Props> = { a: 5 }

手写 Readonly

ts
type MyReadonly<T> = {
  readonly [Key in keyof T]: T[Key]
}

interface Todo {
  title: string
}

const todo: MyReadonly<Todo> = {
  title: 'Delete inactive users'
}

// 报错
todo.title = 'Hello'
type MyReadonly<T> = {
  readonly [Key in keyof T]: T[Key]
}

interface Todo {
  title: string
}

const todo: MyReadonly<Todo> = {
  title: 'Delete inactive users'
}

// 报错
todo.title = 'Hello'

手写 Record

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

type CatName = 'miffy' | 'boris' | 'mordred'

interface CatInfo {
  age: number
  breed: string
}

const cats: MyRecord<CatName, CatInfo> = {
  miffy: { age: 10, breed: 'Persian' },
  boris: { age: 5, breed: 'Maine Coon' },
  mordred: { age: 16, breed: 'British Shorthair' }
}

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

type CatName = 'miffy' | 'boris' | 'mordred'

interface CatInfo {
  age: number
  breed: string
}

const cats: MyRecord<CatName, CatInfo> = {
  miffy: { age: 10, breed: 'Persian' },
  boris: { age: 5, breed: 'Maine Coon' },
  mordred: { age: 16, breed: 'British Shorthair' }
}

cats.boris

手写 Pick

ts
// 遍历联合类型用in
type MyPick<T, K extends keyof T> = {
  [Key in K]: T[Key]
}

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false
}
// 遍历联合类型用in
type MyPick<T, K extends keyof T> = {
  [Key in K]: T[Key]
}

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false
}

手写 Omit

ts
type MyOmit<T, K extends keyof T> = {
  [Key in keyof T as Key extends K ? never : Key]: T[Key]
}

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false
}
type MyOmit<T, K extends keyof T> = {
  [Key in keyof T as Key extends K ? never : Key]: T[Key]
}

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false
}

手写 Exclude

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

type ExcludeResult = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

// 解释

// 当 T 是联合类型时,TypeScript 会自动对每个成员进行分发操作:

// 分发到每个联合成员
('a' extends 'a' ? never : 'a') |
('b' extends 'a' ? never : 'b') |
('c' extends 'a' ? never : 'c')

// 计算结果
never | 'b' | 'c'

// never 在联合类型中被忽略
'b' | 'c'
type MyExclude<T, U> = T extends U ? never : T

type ExcludeResult = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

// 解释

// 当 T 是联合类型时,TypeScript 会自动对每个成员进行分发操作:

// 分发到每个联合成员
('a' extends 'a' ? never : 'a') |
('b' extends 'a' ? never : 'b') |
('c' extends 'a' ? never : 'c')

// 计算结果
never | 'b' | 'c'

// never 在联合类型中被忽略
'b' | 'c'

手写 Extract

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

type T0 = MyExtract<'a' | 'b' | 'c', 'a' | 'f'> // a
type MyExtract<T, U> = T extends U ? T : never

type T0 = MyExtract<'a' | 'b' | 'c', 'a' | 'f'> // a

手写 Parameters

ts
type MyParameters<T extends (...args: any[]) => any> = T extends (...any: infer S) => any ? S : any

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
type MyParameters<T extends (...args: any[]) => any> = T extends (...any: infer S) => any ? S : any

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]

手写 ReturnType

ts
type MyReturnType<T extends Function> = T extends (...args: any) => infer Value ? Value : never

const fn = (v: boolean) => {
  if (v) return 1
  else return 2
}

type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"
type MyReturnType<T extends Function> = T extends (...args: any) => infer Value ? Value : never

const fn = (v: boolean) => {
  if (v) return 1
  else return 2
}

type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"

实现 isEqual

ts
// 如果是普通方法
type SimpleEqual<T, U> = T extends U ? (U extends T ? true : false) : false

type Test = SimpleEqual<string, any> // boolean 而不是 false,因为受 any 影响转成了联合类型

// 更好的方案
type IsEqual<T, U> = (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? true : false

type Test = IsEqual<string, any> // false 而不是true
// 如果是普通方法
type SimpleEqual<T, U> = T extends U ? (U extends T ? true : false) : false

type Test = SimpleEqual<string, any> // boolean 而不是 false,因为受 any 影响转成了联合类型

// 更好的方案
type IsEqual<T, U> = (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? true : false

type Test = IsEqual<string, any> // false 而不是true

类型体操练习

type-challenges

总结

  • TS静态类型检查,在编辑器中提前发现错误,减少生产环境问题,但是只做业务的话还是不要用太深入,权衡开发成本和维护成本
  • 为什么 TS 类型编程可以叫类型体操,因为它是图灵完备的​,递归、条件判断、循环(映射类型)、变量(泛型)​,甚至能实现 ​加减乘除运算​