【TS 演化史 -- 16】数字分隔符和更严格的类属性检查

做者:Marius Schulz
译者:前端小智
来源: https://mariusschulz.com/
点赞再看,养成习惯

本文 GitHub https://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了不少个人文档,和教程资料。欢迎Star和完善,你们面试能够参照考点复习,但愿咱们一块儿有点东西。前端

TypeScript 2.4 为标识符实现了拼写纠正机制。即便我们稍微拼错了一个变量、属性或函数名,TypeScript 在不少状况下均可以提示正确的拼写。git

TypeScript 2.7 支持 ECMAScript 的数字分隔符提案。 这个特性容许用户在数字之间使用下划线(_)来对数字分组(就像使用逗号和点来对数字分组那样)。github

const worldPopulationIn2017 = 7_600_000_000;
const leastSignificantByteMask = 0b1111_1111;
const papayawhipColorHexCode = 0xFF_EF_D5;

数字分隔符不会改变数字字面量的值,但分组令人们更容易一眼就能读懂数字。面试

这些分隔符对于二进制和十六进制一样有用。typescript

let bits = 0b0010_1010;
let routine = 0xC0FFEE_F00D_BED;
let martin = 0xF0_1E_

注意,可能有些反常识,JS 里的数字表示信用卡和电话号并不适当,这种状况下使用字符串更好。微信

当我们将target设置为es2015编译的上述代码时,TypeScript 将生成如下 JS 代码:框架

const worldPopulationIn2017 = 7600000000;
const leastSignificantByteMask = 255;
const papayawhipColorHexCode = 16773077;

in 操做符细化和精确的 instanceof

TypeScript 2.7带来了两处类型细化方面的改动 - 经过执行“类型保护”肯定更详细类型的能力。dom

首先,instanceof操做符如今利用继承链而非依赖于结构兼容性, 能更准确地反映出 instanceof操做符在运行时的行为。 这能够帮助避免一些复杂的问题,当使用 instanceof去细化结构上类似(但无关)的类型时。ide

其次,in操做符如今作为类型保护使用,会细化掉没有明确声明的属性名。函数

interface A { a: number };
interface B { b: string };

function foo(x: A | B) {
    if ("a" in x) {
        return x.a;
    }
    return x.b;
}

更智能的对象字面量推断

在 JS 里有一种模式,用户会忽略掉一些属性,稍后在使用的时候那些属性的值为 undefined

let foo = someTest ? { value: 42 } : {};

在之前TypeScript会查找 { value: number }{}的最佳超类型,结果是 {}。 这从技术角度上讲是正确的,但并非颇有用。

从2.7版本开始,TypeScript 会“规范化”每一个对象字面量类型记录每一个属性, 为每一个 undefined类型属性插入一个可选属性,并将它们联合起来。

在上例中, foo的最类型是 { value: number } | { value?: undefined }。 结合了 TypeScript 的细化类型,这让我们能够编写更具表达性的代码且 TypeScript 也可理解。 看另一个例子:

// Has type
//  | { a: boolean, aData: number, b?: undefined }
//  | { b: boolean, bData: string, a?: undefined }
let bar = Math.random() < 0.5 ?
    { a: true, aData: 100 } :
    { b: true, bData: "hello" };

if (bar.b) {
    // TypeScript now knows that 'bar' has the type
    //
    //   '{ b: boolean, bData: string, a?: undefined }'
    //
    // so it knows that 'bData' is available.
    bar.bData.toLowerCase()
}

这里,TypeScript 能够经过检查 b 属性来细化bar的类型,而后容许咱们访问 bData 属性。

unique symbol 类型和常量名属性

TypeScript 2.7 对ECMAScript里的 symbols有了更深刻的了解,你能够更灵活地使用它们。

一个需求很大的用例是使用 symbols来声明一个类型良好的属性。 好比,看下面的例子:

const Foo = Symbol("Foo");
const Bar = Symbol("Bar");

let x = {
    [Foo]: 100,
    [Bar]: "hello",
};

let a = x[Foo]; // has type 'number'
let b = x[Bar]; // has type 'string'

能够看到,TypeScript 能够追踪到x拥有使用符号 FooBar 声明的属性,由于 FooBar被声明成常量。 TypeScript 利用了这一点,让 FooBar具备了一种新类型: unique symbols

unique symbolssymbols 的子类型,仅可经过调用 Symbol()Symbol.for() 或由明确的类型注释生成。 它们仅出如今常量声明和只读的静态属性上,而且为了引用一个存在的 unique symbols 类型,你必须使用 typeof 操做符。 每一个对 unique symbols的引用都意味着一个彻底惟一的声明身份。

// Works
declare const Foo: unique symbol;

// Error! 'Bar' isn't a constant.
let Bar: unique symbol = Symbol();

// Works - refers to a unique symbol, but its identity is tied to 'Foo'.
let Baz: typeof Foo = Foo;

// Also works.
class C {
    static readonly StaticSymbol: unique symbol = Symbol();
}

由于每一个 unique symbols都有个彻底独立的身份,所以两个 unique symbols类型以前不能赋值和比较。

const Foo = Symbol();
const Bar = Symbol();

// Error: can't compare two unique symbols.
if (Foo === Bar) {
    // ...
}

另外一个可能的用例是使用 symbols作为联合标记。

// ./ShapeKind.ts
export const Circle = Symbol("circle");
export const Square = Symbol("square");

// ./ShapeFun.ts
import * as ShapeKind from "./ShapeKind";

interface Circle {
    kind: typeof ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: typeof ShapeKind.Square;
    sideLength: number;
}

function area(shape: Circle | Square) {
    if (shape.kind === ShapeKind.Circle) {
        // 'shape' has type 'Circle'
        return Math.PI * shape.radius ** 2;
    }
    // 'shape' has type 'Square'
    return shape.sideLength ** 2;
}

更严格的类属性检查

TypeScript 2.7 引入了一个新的编译器选项,用于类中严格的属性初始化检查。若是启用了--strictPropertyInitialization标志,则类型检查器将验证类中声明的每一个实例属性

  • 是否有包含undefined的类型
  • 有一个明确的初始值设定项,或
  • 在构造函数中被明确赋值

--strictPropertyInitialization选项是编译器选项系列的一部分,当设置--strict标志时,该选项会自动启用。 与全部其余严格的编译器选项同样,我们能够将--strict设置为true,并经过将--strictPropertyInitialization设置为false来有选择地退出严格的属性初始化检查。

请注意,必须设置--strictNullCheck标志(经过—strict直接或间接地设置),以便 --strictPropertyInitialization 起做用。

如今,来看看严格的属性初始化检查。若是没有启用--strictpropertyinitialized标志,下面的代码类型检查就能够了,可是会在运行时产生一个TypeError错误:

class User {
  username: string;
}

const user = new User();

// TypeError: Cannot read property 'toLowerCase' of undefined
const username = user.username.toLowerCase();

出现运行时错误的缘由是,username属性值为undefined,由于没有对该属性的赋值。所以,对toLowerCase()方法的调用失败。

若是启用——strictpropertyinitialize,类型检查器将会报一个错误:

class User {
  // Type error: Property 'username' has no initializer
  // and is not definitely assigned in the constructor
  username: string;
}

接下来,看看四种不一样的方法,能够正确地输入User类来消除类型错误。

解决方案1:容许定义

消除类型错误的一种方法是为username属性提供一个包含undefined的类型:

class User {
  username: string | undefined;
}

const user = new User();

如今,username属性保存undefined的值是彻底有效的。可是,当我们想要将username属性用做字符串时,首先必须确保它实际包含的是字符串而不是undefined的值,例如使用typeof

// OK
const username = typeof user.username === "string"
  ? user.username.toLowerCase()
  : "n/a";

解决方案2:显式属性初始化

消除类型错误的另外一种方法是向username属性添加显式初始化。经过这种方式,属性将当即保存一个字符串值,而且不会明显的undefined

class User {
  username = "n/a";
}

const user = new User();

// OK
const username = user.username.toLowerCase();

解决方案3: 使用构造函数赋值

也许最有用的解决方案是将username参数添加到构造函数中,而后将其分配给username属性。这样,每当构造User类的实例时,调用者必须提供用户名做为参数:

class User {
  username: string;

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

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

我们 还能够经过删除对类字段的显式赋值并将public修饰符添加到username构造函数参数来简化User类,以下所示:

class User {
  constructor(public username: string) {}
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

请注意,严格的属性初始化要求在构造函数中全部可能的代码路径中明确分配每一个属性。 所以,如下代码类型不正确,由于在某些状况下,咱们将username属性赋值为未初始化状态:

class User {
  // Type error: Property 'username' has no initializer
  // and is not definitely assigned in the constructor.
  username: string;

  constructor(username: string) {
    if (Math.random() < 0.5) {
      this.username = username;
    }
  }
}

解决方案4:明确的赋值断言

若是类属性既没有显式初始化,也没有undefined的类型,则类型检查器要求直接在构造函数中初始化该属性;不然,严格的属性初始化检查将失败。若是我们但愿在帮助方法中初始化属性,或者让依赖项注入框架来初始化属性,那么这是有问题的。在这些状况下,我们必须将一个明确的赋值断言(!)添加到该属性的声明中:

class User {
  username!: string;

  constructor(username: string) {
    this.initialize(username);
  }

  private initialize(username: string) {
    this.username = username;
  }
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

经过向username属性添加一个明确的赋值断言,这会告诉类型检查器,指望对username属性进行初始化,即便它本身没法检测到这一点。如今我们的责任是确保在构造函数返回后明确地将属性赋值给它,因此必须当心;不然,username 属性可能被明显的undefined或者在运行时就会报 TypeError 错误。

显式赋值断言

尽管我们尝试将类型系统作的更富表现力,但咱们知道有时用户比TypeScript更加了解类型。

上面提到过,显式赋值断言是一个新语法,使用它来告诉 TypeScript 一个属性会被明确地赋值。 可是除了在类属性上使用它以外,在TypeScript 2.7里你还能够在变量声明上使用它!

let x!: number[];
initialize();
x.push(4);

function initialize() {
    x = [0, 1, 2, 3];
}

假设咱们没有在 x后面加上感叹号,那么TypeScript会报告 x从未被初始化过。 它在延迟初始化或从新初始化的场景下很方便使用。


代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

原文:
http://mariusschulz.com/blog/...
https://mariusschulz.com/blog...


交流

文章每周持续更新,能够微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了不少个人文档,欢迎Star和完善,你们面试能够参照考点复习,另外关注公众号,后台回复福利,便可看到福利,你懂的。

相关文章
相关标签/搜索