目录

  • 类型元编程
  • 内置工具类型窥探
  • 内部工具类型举荐
  • 新操作符
  • 申明文件

类型元编程

什么是元编程:

维基百科是这样形容的:元编程是一种编程技术,编写进去的计算机程序可能将其余程序作为数据来解决。意味着能够编写出这样的程序:它可能读取、生成、剖析或者转换其它程序,甚至在运行时批改程序本身。在某些状况下,这使程序员能够最大限度地缩小表白解决方案的代码行数,从而缩小开发工夫。它还容许程序更灵便无效地解决新状况而无需从新编译。

简略的说,元编程可能写出这样的代码:

  • 能够生成代码
  • 能够在运行时批改语言构造,这种景象被称为反射编程(Reflective Metaprogramming)或反射(Reflection)

什么是反射:

反射是元编程的一个分支,反射又有三个子分支:

  1. 自省(Introspection):代码可能自我查看、拜访外部属性,咱们能够据此取得代码的底层信息。
  2. 自我批改(Self-Modification):顾名思义,代码能够批改本身。
  3. 调解(Intercession):字面意思是「代别人行事」,在元编程中,调解的概念相似于包装(wrapping)、捕捉(trapping)、拦挡(intercepting)。

举个理论一点的例子

  • ES6(ECMAScript 2015)中用 Reflect(实现自省)和 Proxy(实现调解) 进行编码操作,称之为是一种元编程。
  • ES6 之前利用 eval 生成额定的代码,利用 Object.defineProperty 扭转某个对象的语义等。

TypeScript 的类型元编程

个人感觉「元编程」这个概念并没有规范的明确的定义,所以本文这里就把在 TypeScript 中应用 infer、keyof、in 等关键字进行操作,称之为是 TypeScript 的类型元编程。或者说是「偏底层一点的个性」或者「骚操作」,大家明确其用处即可。

unknown

unknown type 是 TypeScript 中的 Top Type。符号是(⊤), 换句话说,就是任何类型都是 unknown 的子类型,unknown 是所有类型的父类型。换句最简略的话说,就是 任何值都能够赋值给类型是 unkown 的变量,与其对应的是,咱们不能把一个 unkown 类型的值赋值给任意非 unkown 类型的值。

let a: unknown = undefineda = Symbol('deep dark fantasy')a = {}a = falsea = '114514'a = 1919nlet b : bigint = a; // Type 'unknown' is not assignable to type 'bigint'.

never

never 的行为与 unknown 相同,never 是 TypeScript 中的 Bottom Type,符号是(⊥),换句话说,就是任何类型都是 never 的父类型,never 是所有类型的子类型。

也能够顾名思义,就是「永远不会」=>「不要」的意思,never 与 infer 联合是常见体操姿态,下文会介绍。

let a: never = undefined // Type 'undefined' is not assignable to type 'never'

keyof

能够用于获取对象或数组等类型的所有键,并返回一个联结类型

interface Person {  name: string  age: number}type K1 = keyof Person  // "name" | "age"type K2 = keyof []      // "length" | "toString" | "push" | "concat" | "join"type K3 = keyof { [x: string]: Person }  // string | number

in

在映射类型中,能够对联结类型进行遍历

type Keys = 'firstName' | 'lastName'type Person = {  [key in Keys]: string}// Person: { firstName: string; lastName: string; }

[]

索引操作符,应用 [] 操作符能够进行索引拜访,所谓索引,就是依据肯定的指向返回相应的值,比方数组的索引就是下标 0, 1, 2 等。TypeScript 里的索引签名有两种:字符串索引和数字索引。

字符串索引(对象)

对于纯对象类型,应用字符串索引,语法:T[key]

interface Person {  name: string  age: number}type Name = Person['name']  // Name: string

索引类型自身也是一种类型,因而还能够应用联结类型或者其余类型进行操作

type I1 = Person['name' | 'age']  // I1: string | numbertype I2 = Person[keyof Person]    // I2: string | number

数字索引(数组)

对于类数组类型,应用数字索引,语法:T[number]

type MyArray = ['Alice', 'Bob', 'Eve']type Alice = MyArray[0]       // 'Alice'type Names = MyArray[number]  // 'Alice' | 'Bob' | 'Eve'

理论一点的例子

const PLAYS = [  {    value: 'DEFAULT',    name: '领取送',    desc: '用户领取后即获赠一张券',  },  {    value: 'DELIVERY_FULL_AMOUNT',    name: '满额送',    desc: '用户领取满肯定金额可获赠一张券',    checkPermission: true,    permissionName: 'fullAmount',  },]type Play = typeof PLAYS[number]/*type Play = {    value: string;    name: string;    desc: string;    checkPermission?: undefined;    permissionName?: undefined;} | {    value: string;    name: string;    desc: string;    checkPermission: boolean;    permissionName: string;}*/

泛型(generic)

软件工程中,咱们不仅要创立统一的定义良好的 API,同时也要思考可重用性。组件不仅可能反对以后的数据类型,同时也能反对将来的数据类型,这在创立大型零碎时十分有用。

理论例子,封装 ajax 申请库,反对不同的接口返回它该有的数据结构。

function ajax<T>(options: AjaxOptions): Promise<T> {  // actual logic...}function queryAgencyRole() {  return ajax<{ isAgencyRole: boolean }>({    method: 'GET',    url: '/activity/isAgencyRole.json',  })}function queryActivityDetail() {  return ajax<{ brandName: string; }>({    method: 'GET',    url: '/activity/activityDetail.json',  })}const r1 = await queryAgencyRole()r1.isAgencyRole  // r1 里能够拿到 isAgencyRoleconst r2 = await queryActivityDetail()r2.brandName     // r2 里能够拿到 brandName

extends

在官网的定义中称为条件类型(Conditional Types),能够了解为「三目运算」,T extends U ? X : Y,如果 T 是 U 的子集,那么就返回 X 否则就返回 Y。

  • 个别与泛型配合应用。
  • extends 会遍历联结类型,返回的也是联结类型。
type OnlyNumber<T> = T extends number ? T : nevertype N = OnlyNumber<1 | 2 | true | 'a' | 'b'>  // 1 | 2

通常状况下,散布的联结类型是咱们想要的, 然而也能够让 extends 不遍历联结类型,而作为一个整体进行判断与返回。只须要在 extends 关键字的左右两侧加上方括号 [] 进行润饰即可。

// 散布的条件类型type ToArray<T> = T extends any ? T[] : never;type R = ToArray<string | number>;// type R = string[] | number[]
// 不散布的条件类型type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;type R = ToArrayNonDist<string | number>;// type R = (string | number)[]

infer

infer 关键字能够对运算过程中的类型进行存储,相似于定义一个变量。
内置的工具类型 ReturnType 就是基于此个性实现的。

type ReturnType<T> = T extends (...args: any) => infer R ? R : any;type R1 = ReturnType<() => number>     // R1: numbertype R2 = ReturnType<() => boolean[]>  // R2: boolean[]

递归(recursion)

在 TypeScript 中递归也是调用(或援用)本人,不过不肯定须要跳出。
如下,定义 JSON 对象的规范类型构造。

// 定义根底类型集type Primitive = string | number | boolean | null | undefined | bigint | symbol// 定义 JSON 值type JSONValue = Primitive | JSONObject | JSONArray// 定义以纯对象开始的 JSON 类型interface JSONObject {  [key: string]: JSONValue}// 定义以数组开始的 JSON 类型type JSONArray = Array<JSONValue>

提个小问题:为什么 TypeScript 不跳出递归也不会陷入死循环?

But apart from being computationally intensive, these types can hit an internal recursion depth limit on sufficiently-complex inputs. When that recursion limit is hit, that results in a compile-time error. In general, it’s better not to use these types at all than to write something that fails on more realistic examples.
--from https://www.typescriptlang.or...

typeof

概念:像 TypeScript 这样的古代动态类型语言,个别具备两个搁置语言实体的「空间」,即类型空间(type-level space)和值空间(value-level space),前者用于寄存代码中的类型信息,在运行时会被齐全擦除掉;后者用于寄存代码中的「值」,会保留到运行时。

  • 值空间:变量、对象、数组、class、enum 等。
  • 类型空间:type、interface、class、enum 等。

typeof 的作用是把「值空间」的数据转换成「类型空间」的数据。

const MARKETING_TYPE = {  ISV: 'ISV_FOR_MERCHANT',  ISV_SELF: 'ISV_SELF',  MERCHANT: 'MERCHANT_SELF',}type MarketingType = typeof MARKETING_TYPE/*type MarketingType = {  ISV: string;  ISV_SELF: string;  MERCHANT: string;}*/

as const

as const 是一个类型断言,作用也是把「值空间」的数据转换成「类型空间」的数据,并且设置成只读。

let x = 'hello' as const;   // x: 'hello'let y = [10, 20] as const;  // y: readonly [10, 20]let z = { text: 'hello' } as const;  // z: { readonly text: 'hello' }

理论一点的例子:

const MARKETING_TYPE = {  ISV: 'ISV_FOR_MERCHANT',  ISV_SELF: 'ISV_SELF',  MERCHANT: 'MERCHANT_SELF',} as consttype MT = typeof MARKETING_TYPEtype MarketingType = MT[keyof MT]/*type MT = {  readonly ISV: "ISV_FOR_MERCHANT";  readonly ISV_SELF: "ISV_SELF";  readonly MERCHANT: "MERCHANT_SELF";}type MarketingType = "ISV_FOR_MERCHANT" | "ISV_SELF" | "MERCHANT_SELF"*/

内置工具类型窥探

TypeScript 内置了一些实用的工具类型,能够进步开发过程中类型转换的效率。
基于下面的理解,再来浏览内置工具类型就很轻松了,这里咱们就列举几个罕用或者有代表性的工具类型。

Partial

作用:把对象的每个属性都变成可选属性。

interface Todo {  title: string;  description: string;}type NewTodo = Partial<Todo>/*type NewTodo = {  title?: string;  description?: string;}*/

原理:把每个属性增加 ? 符号,使其变成可选属性。

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

Required

作用:与 Partial 相同,把对象的每个属性都变成必填属性。

interface Todo {  title?: string;  description?: string;}type NewTodo = Required<Todo>/*type NewTodo = {  title: string;  description: string;}*/

原理:给每个属性增加 -? 符号,- 指的是去除,-? 意思就是去除可选,就变成了 required 类型。

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

Readonly

作用:把对象的每个属性都变成只读属性。

interface Todo {  title: string;  description: string;}type NewTodo = Readonly<Todo>/*type NewTodo = {  readonly title: string;  readonly description: string;}*/const todo: Readonly<Todo> = {  title: 'Delete inactive users'}// Cannot assign to 'title' because it is a read-only property.todo.title = "Hello";

原理:给每个属性增加 readonly 关键字,就变成了只读属性。

type Readonly<T> = {  readonly [P in keyof T]: T[P];};

Pick

作用:与 lodash 的 pick 办法一样,筛选对象里须要的键值返回新的对象,不过这里筛选的是类型。

interface Todo {  title: string;  description: string;  completed: boolean;} type TodoPreview = Pick<Todo, 'title' | 'completed'>/*type TodoPreview = {  title: string;  completed: boolean;}*/

原理:应用条件类型束缚传入的联结类型 K,而后再对符合条件的联结类型 K 进行遍历。

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

Omit

作用:与 Pick 工具办法相同,排除对象的某些键值。

interface Todo {  title: string;  description: string;  completed: boolean;} type TodoPreview = Omit<Todo, 'description'>/*type TodoPreview = {  title: string;  completed: boolean;}*/

原理:与 Pick 相似,不过是先通过 Exclude 失去排除后的残余属性,再遍历生成新对象类型。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Exclude

作用:排除联结类型里的一些成员类型。

type T0 = Exclude<'a' | 'b' | 'c', 'a'>        // T0: 'b' | 'c'type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>  // T1: 'c'

原理:通过条件类型 extends 把不须要的类型排除掉。

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

Parameters

作用:获取函数的参数类型,返回的是一个元组类型

type T0 = Parameters<() => string>         // T0: []type T1 = Parameters<(s: string) => void>  // T1: [s: string]

原理:通过 infer 关键字获取函数的参数类型并返回

type Parameters<T extends (...args: any) => any> =   T extends (...args: infer P) => any ? P : never;

ReturnType

作用:获取函数的返回类型

type R1 = ReturnType<() => number>      // R1: numbertype R2 = ReturnType<() => boolean[]>   // R2: boolean[]

原理:通过 infer 关键字获取函数返回类型

type ReturnType<T extends (...args: any) => any> =   T extends (...args: any) => infer R ? R : any;

Awaited

作用:获得无 Promise 包裹的原始类型。

type res = Promise<{ brandName: string }>type R = Awaited<res>  // R: { brandName: string }

原理:如果是一般类型就返回该类型,如果是 Promise 类型,就用 infer 定义 then 的值,并返回。

type Awaited<T> =  T extends null | undefined    ? T    : T extends object & { then(onfulfilled: infer F): any } // 查看 Promise 类型      ? F extends (value: infer V, ...args: any) => any         ? Awaited<V>  // 递归 value 类型        : never       // 不合乎规定的 Promise 类型抛弃      : T;    // 不是 Promise 类型间接返回

Promise 类型形态如下

/** * Represents the completion of an asynchronous operation */interface Promise<T> {    /**     * Attaches callbacks for the resolution and/or rejection of the Promise.     * @param onfulfilled The callback to execute when the Promise is resolved.     * @param onrejected The callback to execute when the Promise is rejected.     * @returns A Promise for the completion of which ever callback is executed.     */    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;    /**     * Attaches a callback for only the rejection of the Promise.     * @param onrejected The callback to execute when the Promise is rejected.     * @returns A Promise for the completion of the callback.     */    catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;}

获取 Promise 类型的另一种简略实现:

type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T

内部工具类型举荐

市面上有 2 款 star 比拟多的开源工具库
type-fest: https://github.com/sindresorh...
utility-types: https://github.com/piotrwitek...
type-fest 没有用过,介绍一下 utility-types 的 ValuesType,比拟罕用。

ValuesType

获取对象或数组的值类型。

interface Person {  name: string  age: number}const array = [0, 8, 3] as consttype R1 = ValuesType<Person>          // string | numbertype R2 = ValuesType<typeof array>    // 0 | 8 | 3type R3 = ValuesType<[8, 7, 6]>       // 8 | 7 | 6

理论例子:获取 JS 常量的值类型,防止重复劳动。

const MARKETING_TYPE = {  ISV: 'ISV_FOR_MERCHANT',  ISV_SELF: 'ISV_SELF',  MERCHANT: 'MERCHANT_SELF',} as consttype MarketingType = ValuesType<typeof MARKETING_TYPE>// type MarketingType = "ISV_FOR_MERCHANT" | "ISV_SELF" | "MERCHANT_SELF"

实现原理:应用上文说到的「字符串索引」和「数字索引」来取值。

type ValuesType<  T extends ReadonlyArray<any> | ArrayLike<any> | Record<any, any>> = T extends ReadonlyArray<any>  ? T[number]  : T extends ArrayLike<any>  ? T[number]  : T extends object  ? T[keyof T]  : never;

新操作符

[2.0] Non-null assertion operator(非空断言符)

断言某个值存在

function createGoods(value: number): { type: string } | undefined {  if (value < 5) {    return  }  return { type: 'apple' }}const goods = createGoods(10)goods.type  // ERROR: Object is possibly 'undefined'. (2532)goods!.type  // ✅

[3.7] Optional Chaining(可选链操作符)

可选链操作符能够跳过值为 null 和 undefined 的状况,只在值存在的状况下才会执行前面的表达式。

let x = foo?.bar()

编译后的后果如下:

let x = foo === null || foo === void 0 ? void 0 : foo.bar();

理论场景的比照:

// beforeif (user && user.address) {  // ...}// afterif (user?.address) {  // ...}// 语法:obj.val?.prop     // 属性拜访obj.val?.[expr]   // 属性拜访obj.arr?.[index]  // 数组拜访obj.func?.(args)  // 函数调用

[3.7] Nullish Coalescing(双问号操作符)

// beforeconst isBlack = params.isBlack || true   // ❌const isBlack = params.hasOwnProperty('isBlack') ? params.isBlack : true  // ✅// afterconst isBlack = params.isBlack ?? true  // ✅

[4.0] Short-Circuiting Assignment Operators(复合赋值操作符)

在 JavaScript 和许多程序语言中,称之为 Compound Assignment Operators(复合赋值操作符)
// Addition// a = a + ba += b;// Subtraction// a = a - ba -= b;// Multiplication// a = a * ba *= b;// Division// a = a / ba /= b;// Exponentiation// a = a ** ba **= b;// Left Bit Shift// a = a << ba <<= b;

新增:

a &&= b   // a && (a = b)a ||= b   // a || (a = b)a ??= b   // a ?? (a = b)

示例:

let values: string[];// Before(values ?? (values = [])).push("hello");// After(values ??= []).push("hello");

申明文件

通常了解就是 .d.ts 文件,按性能能够分为:变量申明、模块申明、全局类型申明、三斜线指令等。

变量申明

如果咱们想应用第三方库 jQuery,一种常见的形式是在 html 中通过 <script> 标签引入 jQuery,而后就能够应用全局变量 $ 或 jQuery 了。假如要获取一个 id 为 foo 的元素。

jQuery('#foo')  // ERROR: Cannot find name 'jQuery'.

TS 会报错,因为编译器不晓得 $ 或 jQuery 是什么,所以须要申明这个全局变量让 TS 晓得,通过 declare var 或 declare let/const 来申明它的类型。

// 申明变量 jQuerydeclare var jQuery: (selector: string) => any;// let 和 var 没有区别,更倡议应用 letdeclare let jQuery: (selector: string) => any;// const 申明的变量不容许被批改declare const jQuery: (selector: string) => any;

申明函数

// 申明函数declare function greet(message: string): void;// 应用greet('hello')

申明类

// 申明类declare class Animal {  name: string;  constructor(name: string);  sayHi(): string;}// 应用const piggy = new Animal('佩奇')piggy.sayHi()

申明对象

// 申明对象declare const jQuery: {  version: string  ajax: (url: string, settings?: any) => void}// 应用console.log(jQuery.version)jQuery.ajax('xxx')

还能够应用 namespace 命名空间来申明对象,晚期 namespace 的呈现是为了解决模块化而发明的关键字,随着 ES6 module 关键字的呈现,为了防止性能混同,当初倡议不应用。

declare namespace jQuery {  const version: string  function ajax(url: string, settings?: any): void;}

模块申明

通常咱们引入 npm 包,它的申明文件可能来源于两个中央:

  • 包内置的类型文件,package.json 的 types 入口。
  • 装置 @types/xxx 对应的包类型文件。

如果下面两种形式都没有找到对应的申明文件,那么就须要手动为它写申明文件了,通过 declare module 来申明模块。

实例:手动修复 @alipay/h5data 的类型反对。

interface H5DataOption {  env: 'dev' | 'test' | 'pre' | 'prod';  autoCache: boolean;}declare module '@alipay/h5data' {  export function fetchData<T extends any>(    path: string,    option?: Partial<H5DataOption>,  ): Promise<T>;}// 应用import { fetchData } from '@alipay/h5data'const res = await fetchData<{ data: 'xxx' }>('url/xxx')

拓展模块类型

某些状况下,模块曾经有类型申明文件了,但引入了一些插件,插件没有反对类型,这时就须要扩大模块的类型。还是通过 declare module 扩大,因为模块申明的类型会合并。

declare module 'moment' {  export function foo(): string}// 应用import moment from 'moment'import 'moment-plugin'moment.foo()

全局类型申明

类型的作用域

在 Typescript 中,只有文件存在 import 或 export 关键字,都被视为模块文件。也就是不论 .ts 文件还是 .d.ts 文件,如果存在上述关键字之一,则类型的作用域为以后文件;如果不存在上述关键字,文件内的变量、函数、枚举等类型都是以全局作用域存在于我的项目中的。

全局作用域申明全局类型

全局作用域内申明的类型皆为全局类型。

部分作用域申明全局类型

部分作用域内能够通过 declare global 申明全局类型。

import type { MarketingType } from '@/constants'declare global {  interface PageProps {    layoutProps: {      marketingType: MarketingType;      isAgencyRole: boolean;    };  }}

三斜线指令

三斜线指令必须放在文件的最顶端,三斜线指令的后面只容许呈现单行或多行正文。
三斜线指令的作用是为了形容模块之间的依赖关系,通常状况下并不会用到,不过在以下场景,还是比拟有用。

  • 当在书写一个依赖其余类型的全局类型申明文件时
  • 当须要依赖一个全局变量的申明文件时
  • 当解决编译后 .d.ts 文件失落的问题

当须要书写一个依赖其余类型的全局类型申明文件时

在全局变量的申明文件中,是不容许呈现 import, export 关键字的,一旦呈现了,那么以后的申明文件就不再是全局类型的申明文件了,所以这时就须要用到三斜线指令。

/// <reference types="jquery" />declare function foo(options: JQuery.AjaxSettings): string;

依赖一个全局变量的申明文件

当须要依赖一个全局变量的申明文件时,因为全局变量不反对通过 import 导入,所以就须要应用三斜线指令来引入了。

/// <reference types="node" />export function foo(p: NodeJS.Process): string;

当解决编译后 .d.ts 文件失落的问题

在写我的项目的时候,我的项目里编写的 .d.ts 文件在 tsc 编译后,并不会搁置到对应的 dist 目录下,这时候就须要手动指定依赖的全局类型。

/// <reference path="types/global.d.ts" />// ValueOf 来自 global.d.tsexport declare type ComplexOptions = ValueOf<typeof complexOptions>;

reference

  • path: 指定类型文件的门路
  • types: 指定类型文件对应的包,例如 对应的类型文件是

参考

TypeScript Handbook:https://www.typescriptlang.or...
TypeScript Learning: https://github.com/Barrior/ty...
你不晓得的 TypeScript 高级技巧:https://www.infoq.cn/article/...
TypeScript 入门教程:https://ts.xcatliu.com/basics...
读懂类型体操:TypeScript 类型元编程根底入门:https://zhuanlan.zhihu.com/p/...
JavaScript 元编程:https://chinese.freecodecamp....
其余材料