使用 keyof 限定对象的属性值参数

如果参数中包含对象的属性,吾辈一般会使用 string 或者 PropertyKey,但实际上 ts 里在这种场景下有更合适的方式: keyof

function get<T extends object>(obj: T, k: PropertyKey): any {
  return obj[k]
}

const i = get({ name: '', age: 17 }, 'age') as number
console.log(i - 1)

优化一下

function get<T extends object>(obj: T, k: keyof T): T[keyof T] {
  return obj[k]
}

// 这里的第二个参数会有类型约束
const i = get({ name: '', age: 17 }, 'age') as number
console.log(i - 1)

将对象的所有值进行映射

ts 内部实现了一个 Partial 类型就是这样

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P]
}

下面是一个使用示例,将对象中所有属性的值映射为函数。

type MapValueToFunc<T> = { [P in keyof T]: () => T[P] }

function mapToComputed<T extends object>(obj: T): MapValueToFunc<T> {
  return Object.keys(obj).reduce((res, k) => {
    res[k] = obj[k]
    return res
  }, {}) as any
}

测试一下

const res = mapToComputed({
  name: 'rx',
  age: 17,
  sex: false,
})

infer 解构

可以将复杂类型进行解构,以得到复杂类型中的部分类型。

下面是几个应用场景

  • 取出泛型类。例如从 Promise<T> 中取出 T
  • 取出函数的参数类型列表/返回值。例如从 (...args: P): R 中取出 PR
  • 获取到构造函数参数列表

取出泛型类

// 一个用于
type PromiseDeconstruct<T extends Promise<any>> = T extends Promise<infer R>
  ? R
  : never
const res = Promise.resolve(1)
// 解构 Promsie 中的泛型类
const i: PromiseDeconstruct<typeof res> = 1

获取函数的类型

type FuncParam<T extends (...args: any[]) => any> = T extends (
  ...args: infer P // 声明一个变量以进行解构部分类型
) => any
  ? P // 这个值其实永远不会到
  : never

function add(a: number, b: string): string {
  return a + b
}

type AddParam = FuncParam<typeof add>

const arr: AddParam = [1, '2']

typeof 获取变量的类型

typeof 在 JS 中原本只是获取变量的类型,而且除了基本类型和 Function 之外,其它的所有类型都会得到 object。而在 TS 种,该关键字的功能得到了增强,它真的变成了可以获取到变量类型,并且参与类型运算了。

  • 如果是 const 声明的基本类型,则会被认为是字面量类型

    const s = ''
    type CustomString = typeof s // ''
    const str1: CustomString = ''
    const str2: CustomString = '1' // Type '"1"' is not assignable to type '""'.ts(2322)
    
  • 如果是 let/var 声明的基本类型变量,则会被正常认为是基本类型

    let s = ''
    type CustomString = typeof s // string
    const str1: CustomString = ''
    const str2: CustomString = '1'
    
  • 如果是对象,则会被认为是对象的真实类型而非 object

    const user = { name: '', age: 17 }
    /**
     * {
     *   name: string;
     *   age: number;
     * }
     */
    type User = typeof user
    const user2: User = { name: 'rx', age: 1 }
    

注:虽然着 TypeScript 的类型运算中是这样的,但实际上使用 console.log(typeof new Date()) 打印的还是 object 而非 Date,请记住:TypeScript 只在编译期生效,运行时所有类型都会被擦除。

as const 声明常量

使用 as const 可以声明一个变量为常量

  • 基本类型:将之变为字面量类型

  • 对象:将之所有的属性变为只读

  • 数组:将之变为元组

    const i = 1 as const
    const str = '1' as const
    const bool = false as const
    const tuple = [1, 2] as const
    const obj = {
      name: 'rx',
      age: 0,
    } as const
    

泛型中指定类型必须拥有某个字段

export function treeMap<
  T extends object,
  C extends { id: keyof T; children: keyof T },
  R extends { [C['children']]: R[] },
>(node: T, fn: (t: T, parentPath: T[C['id']][]) => R, options: C): R

动态根据对象的值进行过滤(Pick/Omit 是静态的)

参考:https://github.com/microsoft/TypeScript/issues/23199#issuecomment-379323872open in new window

type FilteredKeys<T, U> = {
  [P in keyof T]: T[P] extends U ? P : never
}[keyof T]

// 过滤所有值不为 object 的字段
type PickObject<T extends object> = {
  [P in FilteredKeys<T, object>]: T[P]
}
type S = PickObject<{
  name: string
  age: number
  info: {
    age: []
  }
}> // { info: { age: []; }; }

node.js 在 TypeScript 中使用 process.env

TypeScript 中定义 process.env 的类型,默认为 Record<string, string>

使用以下定义可破

// src/@types/environment.d.ts

declare namespace NodeJS {
  interface ProcessEnv {
    GITHUB_AUTH_TOKEN: string
    NODE_ENV: 'development' | 'production'
    PORT?: string
    PWD: string
  }
}

// If this file has no import/export statements (i.e. is a script)
// convert it into a module by adding an empty export statement.
export {}

参考: https://stackoverflow.com/questions/45194598/using-process-env-in-typescriptopen in new window

如果使用的是 import.meta.env.NODE_ENV 的写法,则需要修改成下面这样

// src/env.d.ts
interface ImportMeta {
  env: {
    NODE_ENV: 'development' | 'production'
  }
}

自动推断包含一般值与函数的情况

type GetValue<T> = T | (() => T)
type GetValueType<T> = T extends () => any
  ? ReturnType<Extract<T, () => any>> | Exclude<T, () => any>
  : T

declare function f<T extends any>(
  map: [probability: number, value: T][],
): () => GetValueType<T>

f<GetValue<number>>([
  [1, 1],
  [1, () => 1],
])
f([
  [1, 1],
  [1, () => 1], // The type is not correctly inferred
])

1622083980387

f 的类型定义修改如下

declare function f<T extends any>(
  map: [probability: number, value: T | (() => T)][],
): () => GetValueType<T>

参考答案:https://segmentfault.com/q/1010000040072586/a-1020000040073003open in new window

如何增加新的全局变量

// 这行是必不可少的
export {}

declare global {
  interface Global {
    config: MyConfigType
  }
}

参考:https://stackoverflow.com/questions/57132428/open in new window

如何为第三方包定义类型

在使用某些 npm 模块时,你可能发现 @types/open in new window 下面并没有社区维护的类型定义,这时候你需要自己维护一个类型定义。

大致上有三种方式

  1. 在项目的 src/@types/<module>.d.ts 中编写类型定义
  2. 在 monorepo 项目中创建 types-<module> 模块
  3. 为社区项目 DefinitelyTypedopen in new window 做贡献

现在只说一下第一、第二种方式,假设我们要为名字为 a 的模块定义类型

在项目的 src/@types/a.d.ts 中编写类型定义

// a.d.ts
declare module 'a' {
  // 注意:import 必须卸载 declare module 内部
  import { Plugin } from 'vite'
  export function hello(name: string): Plugin
}

// 使用
import { hello } from 'a'

hello()

在 monorepo 项目中创建 types-a 模块

// a.d.ts
import { Plugin } from 'vite'
export function hello(name: string): Plugin

配置 package.json 导出

{
  "name": "types-a",
  "types": "./a.d.ts"
}

然后在需要的模块安装它即可

强制断言类型

有时候我们知道某些类型应该是什么类型,但 ts 不忍,所以需要绕过去。

type Expect<T, E> = T extends E ? T : never
type Func = (...args: any[]) => any

function f<T, K extends keyof T>(
  o: T,
  k: K,
  ...args: Parameters<Expect<T[K], Func>>
): ReturnType<Expect<T[K], Func>> {
  throw new Error('')
}

const obj = {
  hello(name: string): string {
    return `hello ${name}`
  },
}
f(obj, 'hello', 'liuli')