# 定义

运算符定义一些将对数据, 变量执行的功能。 运算符所作用的数据称为操作数或者操作变量。

# & 运算符

交叉类型

interface Boy {
  handsome: boolean;
}

interface Girl {
  cute: boolean;
}

type Person = Boy & Girl;

const someone: Person = {
	handsome: true,
	cute: false
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# | 运算符

联合类型

interface Boy {
  hair: boolean;
  tall: boolean;
}

interface Girl {
  hair: boolean;
  cute: boolean;
}

type Person = Boy | Girl

const someone: Person = {
  hair: true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# ! 非空断言操作符

表达式不能为 nullundefined 的方式

!在类型检查器无法得出结论的情况下,可以使用新的后缀表达式运算符来断言其操作数为非null且未定义。具体来说,操作x!生成一个类型为x的值,不包含 nullundefined的值。类似于表单<T>x和的类型声明x as T

Note: !非空断言操作符会从编译生成的 JavaScript 代码中移除。

class C {
    foo!: number;
    // ^
    // Notice this '!' modifier.
    // This is the "definite assignment assertion"

    constructor() {
        this.initialize();
    }

    initialize() {
        this.foo = 0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 忽略 null 和 undefined 类型

const profile: any = {
  name: 'Rain120',
  schools: {}
}

const getProfile(profile: any) {
  console.log(profile!.name, profile!.age, profile!.schools!)
}

getProfile(profile);
getProfile();
1
2
3
4
5
6
7
8
9
10
11

# 忽略函数 undefined 类型

const curry = (fn: any) => {
  return fn!();
}
1
2
3

Note:

出现下面情况,需要注意一下,因为!非空断言操作符会从编译生成的 JavaScript 代码中移除。

const name: undefined | string = undefined;
const getName: string = name!;
console.log(getName);

// 转换成 ES5 ===>
"use strict";
const name = undefined;
const getName = name;
console.log(getName);
1
2
3
4
5
6
7
8
9

严格的属性初始化 (opens new window)

非null断言运算符 (opens new window)

# ? 运算符

  • 定义属性用于 可选属性定义
interface Profile {
  name: string;
  age?: number | string;
}
1
2
3
4

optional-properties (opens new window)

  • 使用属性用于 可选的属性访问

# ?. 运算符

?. 只会检查其左侧的值是否为 nullundefined, 而不检查任何后续属性。

const x = foo?.bar.baz

// ===>

const x = (foo === null || foo === undefined)
	? undefined
  : foo.bar.baz();
1
2
3
4
5
6
7

Note: typescript 3.7+才支持。

optional-chaining (opens new window)

Announcing TypeScript 3.7 RC (opens new window) -> 译文 (opens new window)

# ?? 运算符

空值合并运算符 是即将推出的另一个 ECMAScript 2020功能, 它与可选的链接并驾齐驱。

当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数

let x = foo ?? bar();

// ===>

let x = (foo !== null && foo !== undefined)
	? foo
  : bar();
1
2
3
4
5
6
7

nullish-coalescing (opens new window)

# + - 运算符

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

type MutableRequired<T> = {
  -readonly [P in keyof T]-?: T[P]
};  // 移除readonly和?
type ReadonlyPartial<T> = {
  +readonly [P in keyof T]+?: T[P]
};  // 添加readonly和?
1
2
3
4
5
6

改进对映射类型修饰符的控制 (opens new window)

# _ 运算符

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

// Constants
const COULOMB = 8.957_551_787e9; // N-m^2 / C^2
const PLANCK = 6.626_070_040e-34; // J-s
const JENNY = 867_5309; // C-A-L^2
1
2
3
4

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

let bits = 0b0010_1010;
let routine = 0xC0FFEE_F00D_BED;
let martin = 0xF0_1E_
1
2
3

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

数字分隔符 (opens new window)

# # 运算符

TypeScript 3.8 支持在 ECMAScript 中处于 stage-3 (opens new window) 中的私有字段。

class Person {
    #name: string

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

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

不同于正常属性(甚至是使用 private 修饰符声明的属性),私有字段有一些需要记住的规则:

  • 私有字段使用 # 字符作为开始,通常,我们也把这些称为私有名称。
  • 每个私有字段的名字,在被包含的类中,都是唯一的
  • 在 TypeScript 中,像 publicprivate 修饰符不能用于私有字段
  • 私有字段不能在所包含的类之外访问 —— 即使是对于 JavaScript 使用者来说也是如此。通常,我们把这种称为「hard privacy」。

除了「hard privacy」,私有字段的另外一个优点是我们先前提到的唯一性。

正常的属性容易被子类所改写

class C {
  foo = 10;

  cHelper() {
    return this.foo;
  }
}

class D extends C {
  foo = 20;

  dHelper() {
    return this.foo;
  }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用私有字段时,你完全不必对此担心,因为每个私有字段,在所包含的类中,都是唯一的

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

另外有一个值得注意的地方,访问一个有其他类型的私有字段,都将导致 TypeError

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

对于类属性来说,JavaScript 总是允许使用者访问没被声明的属性,而 TypeScript 需要使用者在访问之前先定义声明。使用私有字段时,无论时 .js 文件还是 .ts,都需要先声明。

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}
1
2
3
4
5
6
7
8
9

更多信息,请查看此 PR (opens new window)

# 私有字段与 private 的区别

说到这里使用 # 定义的私有字段与 private 修饰符定义字段有什么区别呢?现在我们先来看一个 private 的示例:

class Person {
  constructor(private name: string) {}
}

let person = new Person("Semlinker");
console.log(person.name);
1
2
3
4
5
6

在上面代码中,我们创建了一个 Person 类,该类中使用 private 修饰符定义了一个私有属性 name,接着使用该类创建一个 person 对象,然后通过 person.name 来访问 person 对象的私有属性,这时 TypeScript 编译器会提示以下异常:

Property 'name' is private and only accessible within class 'Person'.(2341)
1

那如何解决这个异常呢?当然你可以使用类型断言把 person 转为 any 类型:

console.log((person as any).name);
1

通过这种方式虽然解决了 TypeScript 编译器的异常提示,但是在运行时我们还是可以访问到 Person 类内部的私有属性,为什么会这样呢?我们来看一下编译生成的 ES5 代码,也许你就知道答案了:

var Person =  (function () {
    function Person(name) {
      this.name = name;
    }
    return Person;
}());

var person = new Person("Semlinker");
console.log(person.name);
1
2
3
4
5
6
7
8
9

在 TypeScript 3.8 以上版本通过 # 号定义的私有字段编译后会生成什么代码:

class Person {
  #name: string;

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

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}
1
2
3
4
5
6
7
8
9
10
11

以上代码目标设置为 ES2015,会编译生成以下代码:

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) 
  || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
      throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};

var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) 
  || function (receiver, privateMap) {
    if (!privateMap.has(receiver)) {
      throw new TypeError("attempted to get private field on non-instance");
    }
    return privateMap.get(receiver);
};

var _name;
class Person {
    constructor(name) {
      _name.set(this, void 0);
      __classPrivateFieldSet(this, _name, name);
    }
    greet() {
      console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
    }
}
_name = new WeakMap();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

通过观察上述代码,使用 # 号定义的 ECMAScript 私有字段,会通过 WeakMap 对象来存储,同时编译器会生成 __classPrivateFieldSet__classPrivateFieldGet 这两个方法用于设置值和获取值。

ECMAScript Private Fields (opens new window)

# 快来耍耍啊

# 🌰🌰

// template
1

# 游乐场


# 参考答案

// answer
1

# 参考资料

What's new in TypeScript (opens new window)

TypeScript 3.8 (opens new window)

ECMAScript feature: numeric separators (opens new window)

细数 TS 中那些奇怪的符号 (opens new window)