使用 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);
1
2
3
4
5
6

优化一下

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);
1
2
3
4
5
6
7

将对象的所有值进行映射

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

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};
1
2
3
4
5
6

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

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;
}
1
2
3
4
5
6
7
8

测试一下

const res = mapToComputed({
  name: "rx",
  age: 17,
  sex: false,
});
1
2
3
4
5

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;
1
2
3
4
5
6
7

获取函数的类型

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"];
1
2
3
4
5
6
7
8
9
10
11
12
13

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)
    
    1
    2
    3
    4
  • 如果是 let/var 声明的基本类型变量,则会被正常认为是基本类型

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

    const user = { name: "", age: 17 };
    /**
     * {
     *   name: string;
     *   age: number;
     * }
     */
    type User = typeof user;
    const user2: User = { name: "rx", age: 1 };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

注:虽然着 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;
    
    1
    2
    3
    4
    5
    6
    7
    8

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

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;
1
2
3
4
5

动态根据对象的值进行过滤(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: []; }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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 {};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

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

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
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

1622083980387

f 的类型定义修改如下

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

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

如何增加新的全局变量

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

declare global {
  interface Global {
    config: MyConfigType;
  }
}
1
2
3
4
5
6
7
8

参考: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();
1
2
3
4
5
6
7
8
9
10
11

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

// a.d.ts
import { Plugin } from "vite";
export function hello(name: string): Plugin;
1
2
3

配置 package.json 导出

{
  "name": "types-a",
  "types": "./a.d.ts"
}
1
2
3
4

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