Typescript 入门基础篇(三)

677 阅读11分钟

Typescript 入门基础篇(三)

Typescript 入门基础篇(一)

Typescript 入门基础篇(二)

上篇结束了泛型~

枚举

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。

# 数字枚举
enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
Up初始值为1,其余的成员会从1开始自动增长;或者不给Up初始值,这样Up默认为0,依然其他成员从0开始自动增加。要注意每个枚举成员的值都是不一样的。不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。

enum Er {
  A  = getVal(),
  B // 这个时候因为A没有值,这样B需要赋值才行,不然会报错。
}

# 使用枚举: 通过枚举属性访问枚举成员,枚举名字来访问枚举类型。
function respond( rec : string, message : Direction ) : void {}
respond( 'rec', Direction.Up );

# 字符串枚举
在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。(简言之: 必须初始化)因为字符串是没有自增长的行为的,但是可以很好的序列化,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的,字符串美剧允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。
enum Direction {
  Up = 'Up',
  Down = 'Down',
  Left = 'Left',
  Right = 'Right'
};

# 异构枚举
## 计算的和常量成员
每个枚举成员都带有一个值,它可以是常量或计算出来的。
1. 当满足如下条件时,枚举成员被当做是常量:
* 它是美剧的第一个成员且没有初始化器,这种情况下他被赋值为0
* 它不带有初始化器且它之前的枚举成员是一个数字常量,这种情况下,当前枚举成员的值为它上一个枚举成员的值加1。
* 枚举成员使用常量枚举表达式初始化。常量枚举表达式是Typescript表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,他就是一个常量枚举表达式。
  * 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
  * 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
  * 带括号的常量枚举表达式
  * 一元运算符 + , - , ~ 其中之一应用在了常量枚举表达式
  * 常量枚举表达式作为二元运算符 + , - , * , / , % , << , >> , >>> , &, | , ^的操作对象。若常数枚举表达式求值后为NaN或Infinity,则会在编译阶段报错。
2. 除以上情况的枚举成员都被当做是需要计算得出的值。
enum FileAccess {
  // constant members
  None,
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write,
  // computed member
  G = '123'.length
}
## 联合枚举与枚举成员的类型
存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为:
1. 任何字符串字面量(例如: 'foo')
2. 任何数字字面量(例如: 1)
3. 应用了一元 - 符号的数字字面量(例如: -1)
当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义
>1 枚举成员成为了类型(某些成员只能是枚举成员的值))
enum Shape {
  Circle,
  Square
}
interface Circle {
  kind : Shape.Circle,
  radius : number
}
let c : Circle = {
  kind : Shape.Square, // error
  radius : 100
}
>2 枚举类型本身变成了每个枚举成员的联合
enum E {
  Foo,
  Bar
}
function diff(x : E) : void {
  if(x !== E.Foo || x !== E.Bar){
    // error,!==操作不能用于E.Foo和E.Bar类型
  }
}
这个例子里,我们先检查 x是否不是 E.Foo。 如果通过了这个检查,然后 ||会发生短路效果, if语句体里的内容会被执行。 然而,这个检查没有通过,那么 x则 只能为 E.Foo,因此没理由再去检查它是否为 E.Bar。
## 运行时的枚举
枚举是运行时真正存在的对象。
1. 反向映射
数字枚举成员具有反向映射,从枚举值到枚举名字。
enum Core {
  A
}
let a = Core.A;
let nameOfA = Core[a]; // 'A'
引用枚举成员总会生成对属性访问并且永远也不会内联代码。但不会为字符串枚举成员生成反向映射。
2. const 枚举
为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用const枚举。常量枚举通过在美剧上使用const修饰符来定义。
const enum E {
  A = 1,
  B =  A * 2 
}
常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。
3. 外部枚举
用来描述已经存在的枚举类型的形状
declare enum E {
  A = 1,
  B,
  C = 2
}
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。

类型推论

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

# 自动推断
let X = 3;
变量X会被推断为数字。这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

# 最佳通用类型
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。
let x = [ 0, 1, null];
当前元素的类型为: number和null,计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。若没有找到最佳通用类型的话,类型推断的结果为联合类型。

# 上下文类型
Typescript类型推论也可能按照相反的方向进行。按上下文归类会发生在表达式的类型与相处的位置相关时。
window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button);  //<- Error
};
此时检查器使用window.onmousedown函数的类型来判断右边函数表达式的类型。若上下文类型表达式包含了明确的类型信息,上下文的类型则被忽略。
function createZoo(): Animal[] {
    return [new Rhino(), new Elephant(), new Snake()];
}
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。这个例子里,最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。 当然, Animal会被做为最佳通用类型。

类型兼容性

TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。
在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。

interface Named {
    name: string;
}

class Person {
    name: string;
}

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

TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。

interface Named {
    name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;
function greet(n: Named) {
    alert('Hello, ' + n.name);
}
greet(y); // OK
这里的比较过程是递归进行的,检查每个成员及子成员

比较两个函数

首先看他们的参数列表。是否前者的每个参数都可以再后者中找到对应类型的参数。注意的是参数的名字相同与否无所谓,只看他们的类型。

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

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

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

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

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// 参数类型不正确
listenEvent(EventType.Mouse, (e: number) => console.log(e));

可选参数和剩余参数

比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。当一个函数有剩余参数时,它被当做无限个可选参数。常见的函数接受一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用。

function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// 这里回调里x和y都被使用了,但是在回调中作为了可选参数。对于类型系统是不友好的
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

函数重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。

枚举兼容

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。

类兼容

类与对象字面量和接口差不多,但有一点不同: 类有静态部分和实例部分的类型。比较两个类类型的对象时,只有实例的成员会被比较。静态成员和构造函数不在比较的范围内。

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
私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型兼容

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // okay, y matches structure of x

上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // error, x and y are not compatible

在这里,泛型类型在使用时就好比不是一个泛型类型。

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。

let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // Okay because (x: any)=>any matches (y: any)=>any

你也可以关注我的公众号