TypeScript 2.8

TypeScript 2.8

有条件类型

TypeScript 2.8引入了有条件类型,它能够表示非统一的类型。
有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y

上面的类型意思是,若T能够赋值给U,那么类型是X,否则为Y

有条件的类型T extends U ? X : Y或者解析X,或者解析Y,再或者延迟解析,因为它可能依赖一个或多个类型变量。
是否直接解析或推迟取决于:

  • 首先,令T'U'分别为TU的实例,并将所有类型参数替换为any,如果T'不能赋值给U',则将有条件的类型解析成Y。直观上讲,如果最宽泛的T的实例不能赋值给最宽泛的U的实例,那么我们就可以断定不存在可以赋值的实例,因此可以解析为Y
  • 其次,针对每个在U内由推断声明引入的类型变量,依据从T推断到U来收集一组候选类型(使用与泛型函数类型推断相同的推断算法)。对于给定的推断类型变量V,如果有候选类型是从协变的位置上推断出来的,那么V的类型是那些候选类型的联合。反之,如果有候选类型是从逆变的位置上推断出来的,那么V的类型是那些候选类型的交叉类型。否则V的类型是never
  • 然后,令T''T的一个实例,所有推断的类型变量用上一步的推断结果替换,如果T''明显可赋值U,那么将有条件的类型解析为X。除去不考虑类型变量的限制之外,明显可赋值的关系与正常的赋值关系一致。直观上,当一个类型明显可赋值给另一个类型,我们就能够知道它可以赋值给那些类型的所有实例。
  • 否则,这个条件依赖于一个或多个类型变量,有条件的类型解析被推迟进行。
例子
type TypeName =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName;  // "object"

分布式有条件类型

如果有条件类型里待检查的类型是naked type parameter,那么它也被称为“分布式有条件类型”。
分布式有条件类型在实例化时会自动分发成联合类型。
例如,实例化T extends U ? X : YT的类型为A | B | C,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

例子
type T10 = TypeName void)>;  // "string" | "function"
type T12 = TypeName;  // "string" | "object" | "undefined"
type T11 = TypeName;  // "object"

T extends U ? X : Y的实例化里,对T的引用被解析为联合类型的一部分(比如,T指向某一单个部分,在有条件类型分布到联合类型之后)。
此外,在X内对T的引用有一个附加的类型参数约束U(例如,T被当成在X内可赋值给U)。

例子
type BoxedValue = { value: T };
type BoxedArray = { array: T[] };
type Boxed = T extends any[] ? BoxedArray : BoxedValue;

type T20 = Boxed;  // BoxedValue;
type T21 = Boxed;  // BoxedArray;
type T22 = Boxed;  // BoxedValue | BoxedArray;

注意在Boxed<T>true分支里,T有个额外的约束any[],因此它适用于T[number]数组元素类型。同时也注意一下有条件类型是如何分布成联合类型的。

有条件类型的分布式的属性可以方便地用来过滤联合类型:

type Diff = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter = T extends U ? T : never;  // Remove types from T that are not assignable to U

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff void), Function>;  // string | number
type T33 = Filter void), Function>;  // () => void

type NonNullable = Diff;  // Remove null and undefined from T

type T34 = NonNullable;  // string | number
type T35 = NonNullable;  // string | string[]

function f1(x: T, y: NonNullable) {
    x = y;  // Ok
    y = x;  // Error
}

function f2(x: T, y: NonNullable) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}

有条件类型与映射类型结合时特别有用:

type FunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties = Pick>;

type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties = Pick>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames;  // "updatePart"
type T41 = NonFunctionPropertyNames;  // "id" | "name" | "subparts"
type T42 = FunctionProperties;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties;  // { id: number, name: string, subparts: Part[] }

与联合类型和交叉类型相似,有条件类型不允许递归地引用自己。比如下面的错误。

例子
type ElementType = T extends any[] ? ElementType : T;  // Error

有条件类型中的类型推断

现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。
这个推断的类型变量可以在有条件类型的true分支中被引用。
允许出现多个同类型变量的infer

例如,下面代码会提取函数类型的返回值类型:

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

有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:

type Unpacked =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise ? U :
    T;

type T0 = Unpacked;  // string
type T1 = Unpacked;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked>;  // string
type T4 = Unpacked[]>;  // Promise
type T5 = Unpacked[]>>;  // string

下面的例子解释了在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型:

type Foo = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

相似地,在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型:

type Bar = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

当推断具有多个调用签名(例如函数重载类型)的类型时,用最后的签名(大概是最自由的包含所有情况的签名)进行推断。
无法根据参数类型列表来解析重载。

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType;  // string | number

无法在正常类型参数的约束子语句中使用infer声明:

type ReturnType infer R> = R;  // 错误,不支持

但是,可以这样达到同样的效果,在约束里删掉类型变量,用有条件类型替换:

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

预定义的有条件类型

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> — 从T中剔除可以赋值给U的类型。
  • Extract<T, U> — 提取T中可以赋值给U的类型。
  • NonNullable<T> — 从T中剔除nullundefined
  • ReturnType<T> — 获取函数返回值类型。
  • InstanceType<T> — 获取构造函数类型的实例类型。
Example
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude void), Function>;  // string | number
type T03 = Extract void), Function>;  // () => void

type T04 = NonNullable;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(() => T)>;  // {}
type T13 = ReturnType<(() => T)>;  // number[]
type T14 = ReturnType;  // { a: number, b: string }
type T15 = ReturnType;  // any
type T16 = ReturnType;  // any
type T17 = ReturnType;  // Error
type T18 = ReturnType;  // Error

type T20 = InstanceType;  // C
type T21 = InstanceType;  // any
type T22 = InstanceType;  // any
type T23 = InstanceType;  // Error
type T24 = InstanceType;  // Error

注意:Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。我们没有增加Omit<T, K>类型,因为它可以很容易的用Pick<T, Exclude<keyof T, K>>来表示。

改进对映射类型修饰符的控制

映射类型支持在属性上添加readonly?修饰符,但是它们不支持移除修饰符。
这对于同态映射类型有些影响,因为同态映射类型默认保留底层类型的修饰符。

TypeScript 2.8为映射类型增加了增加或移除特定修饰符的能力。
特别地,映射类型里的readonly?属性修饰符现在可以使用+-前缀,来表示修饰符是添加还是移除。

例子
type MutableRequired = { -readonly [P in keyof T]-?: T[P] };  // 移除readonly和?
type ReadonlyPartial = { +readonly [P in keyof T]+?: T[P] };  // 添加readonly和?

不带+-前缀的修饰符与带+前缀的修饰符具有相同的作用。因此上面的ReadonlyPartial<T>类型与下面的一致

type ReadonlyPartial = { readonly [P in keyof T]?: T[P] };  // 添加readonly和?

利用这个特性,lib.d.ts现在有了一个新的Required<T>类型。
它移除了T的所有属性的?修饰符,因此所有属性都是必需的。

例子
type Required = { [P in keyof T]-?: T[P] };

注意在--strictNullChecks模式下,当同态映射类型移除了属性底层类型的?修饰符,它同时也移除了那个属性上的undefined类型:

例子
type Foo = { a?: string };  // 等同于 { a?: string | undefined }
type Bar = Required;  // 等同于 { a: string }

改进交叉类型上的keyof

TypeScript 2.8作用于交叉类型的keyof被转换成作用于交叉成员的keyof的联合。
换句话说,keyof (A & B)会被转换成keyof A | keyof B
这个改动应该能够解决keyof表达式推断不一致的问题。

例子
type A = { a: string };
type B = { b: string };

type T1 = keyof (A & B);  // "a" | "b"
type T2 = keyof (T & B);  // keyof T | "b"
type T3 = keyof (A & U);  // "a" | keyof U
type T4 = keyof (T & U);  // keyof T | keyof U
type T5 = T2;  // "a" | "b"
type T6 = T3;  // "a" | "b"
type T7 = T4;  // "a" | "b"

更好的处理.js文件中的命名空间模式

TypeScript 2.8加强了识别.js文件里的命名空间模式。
JavaScript顶层的空对象字面量声明,就像函数和类,会被识别成命名空间声明。

var ns = {};     // recognized as a declaration for a namespace ns
ns.constant = 1; // recognized as a declaration for var constant

顶层的赋值应该有一致的行为;也就是说,varconst声明不是必需的。

app = {}; // does NOT need to be var app = {}
app.C = class {
};
app.f = function() {
};
app.prop = 1;

立即执行的函数表达式做为命名空间

立即执行的函数表达式返回一个函数,类或空的对象字面量,也会被识别为命名空间:

var C = (function () {
  function C(n) {
    this.p = n;
  }
  return C;
})();
C.staticProperty = 1;

默认声明

“默认声明”允许引用了声明的名称的初始化器出现在逻辑或的左边:

my = window.my || {};
my.app = my.app || {};

原型赋值

你可以把一个对象字面量直接赋值给原型属性。独立的原型赋值也可以:

var C = function (p) {
  this.p = p;
};
C.prototype = {
  m() {
    console.log(this.p);
  }
};
C.prototype.q = function(r) {
  return this.p === r;
};

嵌套与合并声明

现在嵌套的层次不受限制,并且多文件之间的声明合并也没有问题。以前不是这样的。

var app = window.app || {};
app.C = class { };

各文件的JSX工厂

TypeScript 2.8增加了使用@jsx dom指令为每个文件设置JSX工厂名。
JSX工厂也可以使用--jsxFactory编译参数设置(默认值为React.createElement)。TypeScript 2.8你可以基于文件进行覆写。

例子
/** @jsx dom */
import { dom } from "./renderer"

生成:

var renderer_1 = require("./renderer");
renderer_1.dom("h", null);

本地范围的JSX命名空间

JSX类型检查基于JSX命名空间里的定义,比如JSX.Element用于JSX元素的类型,JSX.IntrinsicElements用于内置的元素。
在TypeScript 2.8之前JSX命名空间被视为全局命名空间,并且一个工程只允许存在一个。
TypeScript 2.8开始,JSX命名空间将在jsxNamespace下面查找(比如React),允许在一次编译中存在多个jsx工厂。
为了向后兼容,全局的JSX命名空间被当做回退选项。
使用独立的
code>@jsx</code指令,每个文件可以有自己的JSX工厂。

新的--emitDeclarationsOnly

--emitDeclarationsOnly允许生成声明文件;使用这个标记.js/.jsx输出会被跳过。当使用其它的转换工具如Babel处理.js输出的时候,可以使用这个标记。

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 画漫画的程序员 GitHub仓库,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界