TypeScript

【TypeScript 演化史 — 第八章】字面量类型扩展 和 无类型导入

上一篇更好的类型推断的文章中,解释了 TypeScript 如何用 const 变量和 readonly 属性的字面量始化来推断字面量类型。这篇文章继续讨论这个,扩展和非扩展字面量类型之间的区别。

扩展字面量类型

当使用 const 关键字声明局部变量并使用字面量值初始化它时,TypeScript 将推断该变量的字面量类型

const stringLiteral = "https"; // Type "https"
const numericLiteral = 42;     // Type 42
const booleanLiteral = true;   // Type true

由于 const 关键字,每个变量的值都不能更改,所以字面量类型非常有意义。它保存了关于被赋值的确切信息。

如果如果 let 声明上述的变量,那么每个字面量类型都被扩展为相应的扩展类型:

let widenedStringLiteral = stringLiteral;   // Type string
let widenedNumericLiteral = numericLiteral; // Type number
let widenedBooleanLiteral = booleanLiteral; // Type boolean

const 变量相反,使用 let 声明的变量是可以修改的。如果 TypeScript 为 let 变量推断一个字面量类型,那么尝试为指定的值以外的任何值赋值都会在编译时产生错误。

因此,对于上述每个let变量,都会推断出扩展的类型,枚举字面量也是如此:

enum FlexDirection {
  Row,
  Column
}

const enumLiteral = FlexDirection.Row; //  FlexDirection.Row 类型
let widenedEnumLiteral = enumLiteral; // FlexDirection 类型

总结一下,下面是扩大字面量类型的规则:

  • 字符串字面量类型被扩展为 string 类型
  • 数字字面量类型被扩展为 number 类型
  • 布尔字面量类型被扩展为 boolean 类型
  • 枚举字面量类型被扩展为包含枚举的类型

到目前为止,咱们一直在研究字面量类型的扩展,在必要时自动扩展。现在来看看非扩展字面量类型,如名所示,它们不会自动地扩展。

非扩展字面量类型

可以通过显式地将变量标注为字面量类型来创建非扩展字面量类型的变量

const stringLiteral: "https" = "https"; // 类型 "https" (非扩展)
const numericLiteral: 42 = 42; // 类型 42 (非扩展)

将非扩展字面量类型的变量的值赋给另一个变量,该变量将不会扩展。

let widenedStringLiteral = stringLiteral; // 类型 "https" (非扩展)
let widenedNumericLiteral = numericLiteral; // 类型 42 (非扩展)

非扩展字面量类型的好处

为了理解非扩展字面量类型的是有用的,咱们再来看看扩展字面量类型。在下面的例子中,一个数组是由两个可扩展字符串字面量类型的变量创建的:

const http = "http"; // Type "http" (可扩展)
const https = "https"; // Type "https" (可扩展)

const protocols = [http, https]; // Type string[]

const first = protocols[0]; // Type string
const second = protocols[1]; // Type string

TypeScript 推断数组 protocols 的类型为 string[]。因此,像 firstsecond 这样的数组元素类型被扩展为 string。字面量类型 "http""https" 的概念在扩展过程中丢失了。

如果咱们显式地将这两个常量指定为非扩展类型,则 protocols 数组将被推断为类型 ("http" | "https")[],它表示一个数组,其中仅包含字符串 "http""https":

const http: "http" = "http"; // Type "http" (非扩展)
const https: "https" = "https"; // Type "https" (非扩展

const protocols = [http, https]; // Type ("http" | "https")[]

const first = protocols[0]; // Type "http" | "https"
const second = protocols[1]; // Type "http" | "https"

现在 firstsecond 的类型被推断为 "http" | "https"。这是因为数组类型没有对索引 0 处的值 "http" 和索引 1 处的值 "https" 进行编码。它只是声明该数组只包含两个字面量类型的值,不管在哪个位置。

如果出于某种原因,希望保留数组中字符串字面量类型的位置信息,可以用如下的方式显示指定:

const http = "http"; // Type "http" (可扩展)
const https = "https"; // Type "https" (可扩展)

const protocols: ["http", "https"] = [http, https]; // Type ["http", "https"]

const first = protocols[0]; // Type "http" (非扩展)
const second = protocols[1]; // Type "https" (非扩展)

现在,firstsecond 被推断为各自的非扩展字符串字面量类型。

无类型导入

从TypeScript 2.1 开始处理无类型化导入更加容易。以前,编译器过于严格,当导入一个没有附带类型定义的模块时,会出现一个错误:

从 TypeScript 2.1 开始,如果模块没有类型声明,编译器将不再报错。

现在,导入的 range 函数的类型为 any。这样做的好处是,将现有的 JS 项目迁移到 TypeScrip t可以减少编译时错误。缺点是,不会得到任何自动完成建议或细粒度类型检查,因为编译器对模块或其导出一无所知。

如果过后提供类型声明,例如通过 npm 的类型声明包,它们将优先于默认的任何类型。(否则,将无法为导入的模块提供类型)

对于没有声明文件的模块的导入,在使用了--noImplicitAny编译参数后仍将被标记为错误。

// Succeeds if `node_modules/asdf/index.js` exists
import { x } from "asdf";

支持--target ES2016,--target ES2017--target ESNext

TypeScript 2.1支持三个新的编译版本值--target ES2016,--target ES2017--target ESNext

使用target--target ES2016将指示编译器不要编译ES2016特有的特性,比如**操作符。

同样,--target ES2017将指示编译器不要编译ES2017特有的特性像async/await

--target ESNext则对应最新的ES提议特性支持.

改进any类型推断

以前,如果 TypeScript 无法确定变量的类型,它将选择any类型。

let x;      // 隐式 'any'
let y = []; // 隐式 'any[]'

let z: any; // 显式 'any'.

使用TypeScript 2.1,TypeScript 不是仅仅选择any类型,而是基于你后面的赋值来推断类型。

仅当设置了--noImplicitAny编译参数时,才会启用此选项。

示例

let x;

// 你仍然可以给'x'赋值任何你需要的任何值。
x = () => 42;

// 在刚赋值后,TypeScript 2.1 知道'x'的类型是'() => number'。
let y = x();

// 感谢,现在它会告诉你,你不能添加一个数字到一个函数!
console.log(x + y);
//          ~~~~~
// 错误!运算符 '+' 不能应用于类型`() => number`和'number'。

// TypeScript仍然允许你给'x'赋值你需要的任何值。
x = "Hello world!";

// 并且现在它也知道'x'是'string'类型的!
x.toLowerCase();

现在对空数组也进行同样的跟踪。

没有类型注解并且初始值为[]的变量被认为是一个隐式的any[]变量。变量会根据下面这些操作x.push(value)、x.unshift(value)x[n] = value向其中添加的元素来不断改变自身的类型。

function f1() {
    let x = [];
    x.push(5);
    x[1] = "hello";
    x.unshift(true);
    return x;  // (string | number | boolean)[]
}

function f2() {
    let x = null;
    if (cond()) {
        x = [];
        while (cond()) {
            x.push("hello");
        }
    }
    return x;  // string[] | null
}

隐式 any 错误

这样做的一个很大的好处是,当使用--noImplicitAny运行时,你将看到较少的隐式any错误。隐式any错误只会在编译器无法知道一个没有类型注解的变量的类型时才会报告。

示例

function f3() {
    let x = [];  // 错误:当变量'x'类型无法确定时,它隐式具有'any[]'类型。
    x.push(5);
    function g() {
        x;    // 错误:变量'x'隐式具有'any【】'类型。
    }
}

更好地检查表达式的操作数中的 null/undefined

在TypeScript 2.2中,空检查得到了进一步的改进。TypeScript 现在将带有可空操作数的表达式标记为编译时错误。

具体来说,下面这些会被标记为错误:

  • 如果+运算符的任何一个操作数是可空的,并且两个操作数都不是anystring类型。
  • 如果-***/%<<>>>>>, &, |^运算符的任何一个操作数是可空的。
  • 如果 <><=>=in 运算符的任何一个操作数是可空的。
  • 如果 instanceof 运算符的右操作数是可空的。
  • 如果一元运算符+-~++或者--的操作数是可空的。

来看看如果咱们不小心,可空表达式操作数就会坑下咱们的情况。在 TypeScript 2.2 之前,下面这个函数是可以很好地编译通过的:

function isValidPasswordLength(
  password: string,
  min: number,
  max?: number
) {
  return password.length >= min && password.length <= max;
}

注意max参数是可选的。这意味着咱们可以使用两个或三个参数来调用isValidPasswordLength

isValidPasswordLength("open sesame", 6, 128); // true
isValidPasswordLength("open sesame", 6, 8); // false

密码 "open sesame"的长度为10个字符。因此,对于长度范围 [6,128] 返回 true,对于长度范围[6,8]返回false,到目前为止,一切 ok。

如果调用isValidPasswordLength且不提供max参数值,那么当密码长度超过 min 值时,咱们可能希望返回 true。然而,事实并非如此:

isValidPasswordLength("open sesame", 6); // false

这里的问题在于 <= max 比较。如果maxundefined,那么 <= max 的值永远都为false。在这种情况下,isValidPasswordLength将永远不会返回true

在 TypeScript 2.2 中,表达式password.length <= max不正确的类型,如果你的应用程序正在严格的null检查模式下运行:

function isValidPasswordLength(
  password: string,
  min: number,
  max?: number
) {
  return password.length >= min  && password.length <= max; // Error: 对象可能为“未定义”.
}

如果操作数的类型是nullundefined或者包含nullundefined的联合类型,则操作数视为可空的。

注意:包含nullundefined的联合类型只会出现在--strictNullChecks模式中,因为常规类型检查模式下nullundefined在联合类型中是不存在的。

那么要怎么修正这个问题呢?一种的解决方案是为max参数提供一个默认值,它只在传递undefined 时起作用。这样,该参数仍然是可选的,但始终包含类型为number的值

function isValidPasswordLength(
  password: string,
  min: number,
  max: number = Number.MAX_VALUE
) {
  return password.length >= min && password.length <= max;
}

当然咱们也可以选择其他的方法,但是我觉得这个方法很好。只要不再将maxundefined 的值进行比较,就可以了

混合类

TypeScript 的一个目的是支持不同框架和库中使用的通用 JS 模式。从TypeScript 2.2开始,增加了对 ES6 混合类(mixin class)模式。接下来讲讲 mixin 是什么,然后举例说明了如何在 TypeScript 中使用它们。

JavaScript/TypeScript中的 mixin

混合类是实现不同功能方面的类。其他类可以包含 mixin 并访问它的方法和属性。这样,mixin 提供了一种基于组合行为的代码重用形式。

混合类指一个extends(扩展)了类型参数类型的表达式的类声明或表达式. 以下规则对混合类声明适用:

  • extends表达式的类型参数类型必须是混合构造函数.
  • 混合类的构造函数 (如果有) 必须有且仅有一个类型为any[]的变长参数, 并且必须使用展开运算符在super(...args)调用中将这些参数传递。

定义完成之后,来研究一些代码。下面是一个 Timestamped 函数,它在timestamp 属性中跟踪对象的创建日期:

type Constructor<T = {}> = new (..args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now()
  }
}

这看起来有点复杂,咱们一行一行来看看:

type Constructor<T = {}> = new (..args: any[]) => T;

type Constructor <T>是构造签名的别名,该签名描述了可以构造通用类型T的对象的类型,并且其构造函数接受任意数量的任何类型的参数。

接下来,让我们看一下mixin函数本身:

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now();
  };
}

Timestamped 函数接受一个名为Base的参数,该参数属于泛型类型 TBase注意TBase 必须与Constructor兼容,即类型必须能够构造某些东西。

在函数体中,咱们创建并返回一个派生自Base的新类。这种语法乍一看可能有点奇怪。咱们创建的是类表达式,而不是类声明,后者是定义类的更常用方法。咱们的新类定义了一个timestamp的属性,并立即分配自UNIX时代以来经过的毫秒数。

注意,从mixin函数返回的类表达式是一个未命名的类表达式,因为class关键字后面没有名称。与类声明不同,类表达式不必命名。咱们可以选择添加一个名称,它将是类主体的本地名称,并允许类引用自己

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class Timestamped extends Base {
    timestamp = Date.now();
  };
}

现在已经介绍了两个类型别名和mixin函数的声明,接下来看看如何在另一个类中使用 mixin:

class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

// 通过将`"Timestamped"`混合到"User"中创建一个新类
const TimestampedUser = Timestamped(User);

// 实例化新的 "TimestampedUser" 类
const user = new TimestampedUser("前端小智")

// 现在,咱们可以同时从User 类中访问属性
// 也可以从 Timestamped 类中访问属性
console.log(user.name);
console.log(user.timestamp);

TypeScript 编译器知道我们在这里创建并使用了一个mixin,一切都是完全静态类型的,并且会自动完成和重构。

混合构造函数

现在,看看一个稍微高级一点的 mixin,类中定义一个构造函数

function Tagged<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    tag: string | null;

    constructor(...args: any[]) {
      super(...args);
      this.tag = null;
    }
  };
}

如果在混合类中定义构造函数,那么它必须有一个类型为any[]rest参数。这样做的原因是,mixin不应该绑定到具有已知构造函数参数的特定类;因此,mixin应该接受任意数量的任意值作为构造函数参数。所有参数都传递给Base的构造函数,然后mixin执行它的任务。在咱们的例子中,它初始化 tag 属性。

混合构造函数类型指仅有单个构造函数签名,且该签名仅有一个类型为 any[] 的变长参数,返回值为对象类型. 比如, 有 X 为对象类型, new (...args: any[]) =X 是一个实例类型为 X 的混合构造函数类型。

以前面使用Timestamped的相同方式来使用混合Tagged

// 通过 User 作为混合 Tagged 来创建一个新类
const TaggedUser = Tagged(User);

// 实例化 "TaggedUser" 类
const user = new TaggedUser("John Doe");

// 现在,可以从 User 类访问属性和 Tagged 中的属性

user.name = "Jane Doe";
user.tag = "janedoe";

mixin 与方法

到目前为止,咱们只在mixin中添加了数据属性。现在来看看另一个 mixin,它额外实现了两个方法:

fucntion Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActivated = false;

    activate() {
      this.isActivated = true;
    }

    deactivate() {
      this.isActivated = false;
    }
  }
}

咱们从mixin函数返回一个常规的类。这意味着咱们可以使用所有受支持的类功能,例如构造函数,属性,方法,getter/setter,静态成员等。

如何所示,咱们如何在 User 类中使用混合的 Activatable

const ActivatableUser = Activatable(User);

// 实例化新的"ActivatableUser"类
const user = new ActivatableUser("John Doe");

//初始化,isActivated 的值为 false
console.log(user.isActivated);

user.activate();

console.log(user.isActivated); // true

组合多个mixin

组合的mixin,可以让它更加灵活。一个类可以包含任意多的mixin,为了演示这点,咱们把上面提到的所有mixin 代码组合在一起。

const SpecialUser = Activatable(Tagged(Timestamped(User)));
const user = new SpecialUser("John Doe");

当然 SpecialUser类不一定非常有用,但关键是,TypeScript静态地理解这种mixin组合。编译器可以类型检查所有的使用,并在自动完成列表中建议可用的成员:

与类继承进行对比,有个区别:一个类只能有一个基类。继承多个基类在 JS 中不行的,因此在 TypeScript中也不行。

原文:
https://mariusschulz.com/blog/null-checking-for-expression-operands-in-typescript

(0)

本文由 Web秀 作者:Javan 发表,转载请注明来源!

热评文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注