TypeScript特点2

类型兼容性

TS中的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。 与Java不同,Java的强制转换是基于继承的,只有有继承关系的对象才可以进行转换。

即只要对象中的成员有包含关系,则可以进行类型转换。如,

1
2
3
4
5
6
7
8
9
10
11
interface Named {
name: string;
}

class Person {
name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

这一点也是根据JS的灵活性设计的,JS中变量类型是自动检测的,程序员不需要专门进行规定。

对象的兼容

TS结构化类型系统的基本规则是,如果x要兼容y,(所谓x兼容y,就是x可以被赋值为y),如

1
2
3
4
5
6
7
8
interface Named {
name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;

称为x兼容y

这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。 在这个例子中,y必须包含名字是namestring类型成员。y满足条件,因此赋值正确。

函数的兼容

函数参数兼容

函数的兼容与对象的兼容相反,函数x的参数包含函数y的参数,则x兼容y。如

1
2
3
4
5
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。

第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。

这里可以理解为,在函数赋值时,函数的参数可以被忽略的,(注意:函数调用的时候仍然必须赋值相等的参数,否则采用可选参数)但是限制了不能多余参数。即y中的参数s,在被赋值给x的时候直接被忽略,即

1
2
3
4
5
6
7
//JS中函数调用的灵活性
function f(a, b){
//...
}

f(1, 2); //参数b被忽略
f(1, 2, 3) //多加参数c

函数返回值兼容

返回参数少的函数可以被赋值为参数多的函数(这里TS称参数多的为参数少的函数的子类),即源函数可以被赋值为子类,即向下转换,如

1
2
3
4
5
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error, because x() lacks a location property

类的兼容

类的兼容只比较实例成员。静态部分不会被比较。(仍然是基于成员的比较,而不是Java中的继承),如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}

class Size {
feet: number;
constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s; // OK
s = a; // OK

高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person Serializable Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

通俗的讲,就是包含所有成员类型的成员属性。如:

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
function extend<T, U>(first: T, second: U): T & U {				//将T与U成员变量混合成为新的变量,并返回该变量,返回时采用交叉类型而不是any
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}

class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

联合类型(Union Types)

形式:A|B|C

含义:

作为参数时,仅支持A类或B类或C类,其他类型数据均不被接接受。如:

1
2
3
4
5
6
7
function f(value: number|string){
//...
}

f(1); //正常
f("1"); //正常
f(true); //失败

作为返回值时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Bird {
fly();
layEggs();
}

interface Fish {
swim();
layEggs();
}

function getSmallPet(): Fish | Bird {
// ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors

由于返回的是A|B,所以我们只能访问A与B的交叉成员。

类型保护与区分类型(Type Guards and Differentiating Types)

联合类型可以使我们接受多种特定类型的参数,但是我们要在函数内部更具参数类型执行特定的操作时又该如何判断呢。下面的写法是错误的,因为联合类型在访问任何独有参数时,都会发生错误。

1
2
3
4
5
6
7
8
9
let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}

为了使代码工作,必须使用类型断言

1
2
3
4
5
6
7
8
let pet = getSmallPet();

if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}

用户自定义的类型保护

注意到,这种写法,我们在任何时候调用参数属性时,都必须加上类型断言。我们可以采用另外一种写法使之后的参数调用不再加上类型断言。

TS中的类型保护机制使之成为现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词

1
2
3
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}

在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用 isFish时,TypeScript会将变量缩减为那个具体的类型(在以后对应分支中的参数的每次调用都会被识别为该类型),只要这个类型与变量的原始类型是兼容的。

1
2
3
4
5
6
7
8
// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
typeof类型保护

实际上我们可以发现,在上面的断言函数中执行的内容就是判断参数是否为对应类型,是返回true,否返回false,所以我们在断言函数内部可以使用typeof来进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isNumber(x: any): x is number {
return typeof x === "number";
}

function isString(x: any): x is string {
return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}

更进一步,对于原始类型(number, string, boolean, symbol),TS直接将typeof识别为断言函数,而不必我们每次都为原始类型类型编写一个断言。

1
2
3
4
5
6
7
8
9
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}

这些* typeof类型保护*只有两种形式能被识别: typeof v === "typename"typeof v !== "typename""typename"必须是 "number""string""boolean""symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

对于非原始类型,如何做到类型判断并收紧呢?TS提供了instanceof类型保护,也可以避免我们重写断言函数。

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
30
31
32
33
interface Padder {
getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}

class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}

function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

  1. 此构造函数的 prototype属性的类型,如果它的类型不为 any的话
  2. 构造签名所返回的类型的联合

null和undefined

TS将null,undefined视为两种不同的类型,他们不能被赋值给任何其他类型的参数。如官方文档所说:

注意,按照JavaScript的语义,TypeScript会把 nullundefined区别对待。 string | nullstring | undefinedstring | undefined | null是不同的类型。

可选参数与可选属性

使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:

1
2
3
4
5
6
7
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

也就是说,可选属性可以被赋值为undefined作为占位参数,但是不能被赋值为null。

同样的,可选属性也可被赋值为undefined作为占位参数。

1
2
3
4
5
6
7
8
9
10
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型别名

形如C中的typedef语法,TS提供了类型别名。

1
2
3
4
5
6
7
8
9
10
11
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}

起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

1
type Container<T> = { value: T };

我们也可以使用类型别名来在属性里引用自己:

1
2
3
4
5
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}

类型别名只能出现在申明的左侧。

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// error! should not pass null or undefined.
}
}
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

1
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

数字字面量类型

TypeScript还具有数字字面量类型。

1
2
3
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}

我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:

1
2
3
4
5
6
function foo(x: number) {
if (x !== 1 || x !== 2) {
// ~~~~~~~
// Operator '!==' cannot be applied to types '1' and '2'.
}
}

换句话说,当 x2进行比较的时候,它的值必须为 1,这就意味着上面的比较检查是非法的。

可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

  1. 具有普通的单例类型属性— 可辨识的特征
  2. 一个类型别名包含了那些类型的联合— 联合
  3. 此属性上的类型保护。
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}

首先我们声明了将要联合的接口。 每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做 可辨识的特征标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:

1
type Shape = Square | Rectangle | Circle;

现在我们使用可辨识联合:

1
2
3
4
5
6
7
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}

Symbols

这个部分也是ES6的引入一种新的原生类型,如numberstring

其特点是唯一,一个symbols是唯一独特的,无法改变的。即使两个symbol输入的key一样,这两个值仍然是完全不同的。

symbol类型的值是通过Symbol构造函数创建的。

1
2
3
let sym1 = Symbol();

let sym2 = Symbol("key"); // 可选的字符串key

Symbols是不可改变且唯一的。

1
2
3
4
let sym2 = Symbol("key");
let sym3 = Symbol("key");

sym2 === sym3; // false, symbols是唯一的

像字符串一样,symbols也可以被用做对象属性的键。

1
2
3
4
5
6
7
let sym = Symbol();

let obj = {
[sym]: "value"
};

console.log(obj[sym]); // "value"

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :