本节介绍ts中的高级类型及Symbols相关内容,高级类型包括交叉类型、联合类型、类型保护、类型别名等内容,Symbols是ECMAScript 2015后的原生类型,像其他的基础类型number和string一样,通过Symbol构造函数创建,Symbol是不可改变且唯一的。
ts中除了基础类型、类类型、枚举、泛型等外还有一些特有的高级类型,包括交叉类型、联合类型等。
交叉类型是将多个类型进行联合,联合后新成一个新的类型,新的类型包含被联合的多个类型的所有特性,如:A&B&C同时具有A和B和C的特性,并且都能兼容。
在mixins中交叉类型比较常见,mixins在后续章节中会介绍,示例如下:
function extend<T, U>(first: T, second: U): T & U {
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 A{
constructor(public name: string) { }
}
interface B{
log(): void;
}
class C implements B{
log() {
// ...
}
}
var jim = extend(new A("Jim"), new C());
var n = jim.name;
jim.log();
上述示例中对A和C进行了mixins处理,即将A类型中的所有成员及C类型中的所有成员进行合并,合并到新的成员中,最后的结果变量jim包含了A和B及C三个类型的所有成员,合并是通过对类原型进行复制操作来实现,最终的结果是传入的T和U的联合类型。
联合类型是标识几种类型之一,使用|分隔每个类型,如:number|string|boolean表示既可以是number,也可以是string,也可是boolean。如果一个值是联合类型,只能访问联合类型的所有类型里的共有成员,如下:
interface A{
a();
b();
}
interface B{
b();
c();
}
function C():A|B{}
let c = C();
c.b()//可以调用
c.a()//报错,因为a不是共同具有的成员。
也可以使用any传递参数,any传递时可以传递任何类型的参数,但是如果传入的参数将没有限制,在函数处理时就有可能出现问题。所以最好使用联合类型,使用了联合类型之后,将限定参数的类型范围,只能是指定的几种类中的一种,这样参数的类型就是可预测的,而不像any一样参数的类型不可预测,会导致后续代码处理时出现不确定性。
使用联合类型时,由于类型有可能是联合类型的任何一种,当想明确的知道具体的类型的到底是那种的时候,需要进行特出判断或者类型断言,如下:
interface A{
a();
b();
}
interface B{
b();
c();
}
function C():A|B{}
let c = C();
if(c.a){
c.a();
}else if(c.c){
c.c();
}
上述实例js中没有问题,但是ts中会报错,因为c的类型不确定,即使使用了if判断,类型也不确定,此时可以使用类型断言进行类型判断:
interface A{
a();
b();
}
interface B{
b();
c();
}
function C():A|B{}
let c = C();
if((<A>c).a){
(<A>c).a();
}else if((<B>c).c){
(<B>c).c();
}
使用了类型断言之后,就确定了变量c的具体类型,ts中将不会报错。
function isA(pet: A| B): pet is A {
return (<A>pet).a!== undefined;
}
上述实例中pet is A就是类型谓词,谓词是pama is Type的格式,pama必须是当前函数里的一个参数名。
调用isA时,ts会将变量缩减为具体的类型,只要类型与变量的原始类型是兼容的就可以,ts不仅会知道is分支中变量的类型,还知道else分支里的类型,如下:
if(isA(c)){
c.a();
}else{
c.c();
}
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}'.`);
}
此实例中使用typeof进行参数类型的判断,判断后就可以确定参数具体的类型了,但是这样每个原始类型的判断都需要定义一个对应的函数,ts中可以简化,不必要实现isString和isNumber函数,使用typeof x === “number”后ts会自动识别为一个类型保护,如下:
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并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
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'
}
上述实例中getRandomPadder函数的返回值有两种类型,SpaceRepeatingPadder或StringPadder,在使用返回的值的时候通过instanceof判断具体的类型,instanceof的右侧需要时一个构造函数,上述实例中SpaceRepeatingPadder的构造函数的参数类型是number,而StringPadder的构造函数的参数类型是string,所以可以通过instanceof进行判断具体的类型,ts检测是将按以下要求检测:
类型别名顾名思义,就是给一个类型起一个新的名字,类型别名有时候和接口很像,但可以作用与原始值,联合类型,元组及其它任何需要手写的类型,如下:
type str = string;
type strResolver = () => string;
type strOrResolver = str | strResolver;
function getStr(n: strOrResolver): str {
if (typeof n === 'string') {
return n;
}else {
return n();
}
}
别名并不会新建类型,而是创建了一个新的名字来引用对应的类型,和接口一样,类型别名也可以泛型,可以添加类型参数并且在别名声明的右侧传入:
type Ct<T> = { value: T };
也可以使用类型的别名来在属性里引用自己:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
类型别名不能出现在声明语句的右侧:
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
类型别名不能出现在声明右侧的任何地方:
type Yikes = Array<Yikes>; // error
接口和类型别名的区别:类型别名可以和接口一样,但是也有一些区别,具体如下:
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;//此处鼠标悬浮时提示的是对象字面量类型
declare function interfaced(arg: Interface): Interface;//此处鼠标悬浮时提示的是Interface类型
字符串字面量类型允许指定字符串必须的固定值,可以和联合类型,类型保护和类型别名很好的配合,通过结合可以实现类似枚举类型的字符串:
type Op = "in" | "out" | "in-out";
class UIElement {
animate(dx: number, dy: number, op: Op) {
if (op=== "in") { }
else if (op=== "out") {}
else if (op=== "in-out") {}
else {}
}
}
let button = new UIElement();
button.animate(0, 0, "in");
button.animate(0, 0, "unop"); // 异常,此处不能运行unop类型
上述示例中只能从三种允许的字符串中选择其中一个来作为参数传递,传入其它的值则会报错,提示传入的值不是指定的类型范围内。
字符串子面量类型也可以用于区分函数重载:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}
可以合并字符串字面量类型,联合类型,类型保护和类型别名来创建可辨识联合的高级模式,被称作标签联合或代数数据类型,具有以下要素:
interface A{
kind: "a";
size: number;
}
interface B{
kind: "b";
width: number;
height: number;
}
interface C{
kind: "c";
radius: number;
}
首先声明将要联合的接口,每个接口都有相同的属性,但有不同的字符串字面量类型,相同的属性称做可辨识的特征或标签,其它的属性则特定于各个接口,将类型进行联合,联合后进行使用可辨识联合,如下:
type D= A|B| C;
function fun(s: A) {
switch (s.kind) {
case "a": return s.size * s.size;
case "b": return s.height * s.width;
case "c": return Math.PI * s.radius ** 2;
}
}
当没有涵盖所有的可辨识联合变化时,想让编译器可以通知,如上述的示例中再添加E类型,同时需要更新fun函数:
type D= A| B| C| E;
function area(s: D) {
switch (s.kind) {
case "a": return s.size * s.size;
case "b": return s.height * s.width;
case "c": return Math.PI * s.radius ** 2;
}
// 此处应该有异常,因为缺少E类型的case操作
}
要实现让编译器可以通知有两种方式:
function fun(s: D): number { // 因为没有caseE类型,所以返回值应该是number|undefined
switch (s.kind) {
case "a": return s.size * s.size;
case "b": return s.height * s.width;
case "c": return Math.PI * s.radius ** 2;
}
}
2.使用never类型,进行完整性检查:
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function fun(s: D) {
switch (s.kind) {
case "a": return s.size * s.size;
case "b": return s.height * s.width;
case "c": return Math.PI * s.radius ** 2;
default: return assertNever(s); // 如果有忽略的case,此处将有异常
}
}
上述示例中assertNever检查s是否为never类型,即为除去所有可能情况后剩下的类型,如果忘记了某个case,那么s将具有一个赶写的类型,因此会得到一个错误。
8. 多态的this类型
多态的this类型表示某个包含类或接口的子类型,能很容易的表现连贯接口间的继承,如下:
class Base{
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
}
let v = new Base(2).multiply(5).add(1).currentValue();
由于Base类使用了this类型,可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变:
class A extends Base{
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
}
let v = new A(2).multiply(5).sin().add(1).currentValue();
如果没有this类型,A就不能在继承Base的同时还保持接口的连贯性,multiply方法会返回Base,并没有sin方法,但使用this类型,multiply会返回this,此处的this就是A类型。
ES6即ECMAScript 2015之后,新加了symbol成为了一个新的原生类型,像其它nubmer和string一样。通过Symbol函数创建:
let s = Symbol();
let b = Symbol(“3232”);
Symbol类型是不可改变且唯一的,即使值一样也是唯一的,如下:
let a = Symbol(‘1’);
let b = Symbol(‘1’);
a === b;//此处比较时会得到false,因为symbol的值是唯一的
symbols也可以被用做对象属性的键:
let a = Symbol();
let obj = {
[a]:’111’
}
obj[a];
Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员:
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol](){
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
当对象实现了Symbol.iterator属性时,就认为它是可迭代的,一些类型已经内置了此属性如:Array、Map、Set、String、Int32Array、Uint32Array等已经实现了各自的Symbol.interator,对象上的Symbol.interator函数负责返回供迭代的值。
for…of语句:
for…of会遍历可迭代的对象,会调用对象上的Symbol.interator方法:
let arr = [1,’2’,false];
for(let e of arr){
e;//1,’2’,false
}
for…in语句:
和for…of语句一样,都可迭代一个列表,但获取的值却不同,for…in迭代的是对象的键列表,for…of则迭代对象键对应的值。
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // "4", "5", "6"
}
for…in可以操作任何对象,可以查看对象的属性,但是for…of只能迭代对象的值:
let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";
for (let pet in pets) {
console.log(pet); // "species"
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
当编译目标是ES5或ES3,迭代器只允许在Array类型上使用,在非数组值上使用时,即使实现了Symbol.interator,for…of语句也会得到一个错误。对于Array,编译器会生成一个简单的for循环代替:
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(num);
}
目标是ES6或更高的时候,编译器会生成相应引擎的for…of内置迭代器实现。