前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【TS 演化史 -- 16】数字分隔符和更严格的类属性检查

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

作者头像
前端小智@大迁世界
发布2020-05-12 12:20:02
1.2K0
发布2020-05-12 12:20:02
举报
文章被收录于专栏:终身学习者终身学习者

作者:Marius Schulz 译者:前端小智 来源:https://mariusschulz.com/

TypeScript 2.4 为标识符实现了拼写纠正机制。即使咱们稍微拼错了一个变量、属性或函数名,TypeScript 在很多情况下都可以提示正确的拼写。

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

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

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

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

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带来了两处类型细化方面的改动 - 通过执行“类型保护”确定更详细类型的能力。

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

其次,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...

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • in 操作符细化和精确的 instanceof
  • 更智能的对象字面量推断
  • unique symbol 类型和常量名属性
  • 更严格的类属性检查
    • 解决方案1:允许定义
      • 解决方案2:显式属性初始化
        • 解决方案3: 使用构造函数赋值
          • 解决方案4:明确的赋值断言
          • 显式赋值断言
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档