typescript(以下简称TS)出来也有好长时间了,下面记录一下学习心得。
首先学这门语言前,请确保有以下基础知识:
- 扎实的javascript基础知识
- es6的基础知识
- 面向对象编程的概念(没有也可以,就当是重新学一遍了)
接下来看一下TS的一些概念:
一、基本类型
TS的基础类型有:字符串(string)、数字(number)、布尔值(boolean)、空(null)、未定义(undefined)、数组(array)、对象(object)、元组(tuple)、枚举(enum)、any、void、never等12种。
写法为在变量后加冒号然后跟变量类型的方式,例如:
1.字符串
写法:
let str: string = 'str';
2.数字
写法:
let num: number = 123;
3.布尔值
写法:
let bol: boolean = false;
4.null
写法:
let n: null = null;
5.undefined
写法:
let u: undefined = undefined;
6.数组
写法:
let arr: number[] = [1,23,4,]; let arr1: Array<number> = [1,2,3];// 使用泛型的方式声明变量
7.对象
写法:
let obj: object={};
8.元组
写法:
let tuple: [number,string] = [12,'3'];
9.枚举
写法:
enum Num{ one=1,// 从几开始,默认为从0开始 two,// 2 three// 3 };
10.any
写法:
let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; let anyArr: any = [1,2,'4',false,null];
11.viod
写法:
function warnUser(): void { console.log("This is my warning message"); } let unusable: void = undefined; let unuse: void;
12.never
写法:
function error(message: string): never { throw new Error(message); } // 推断的返回值类型为never function fail() { return error("Something failed"); } // 返回never的函数必须存在无法达到的终点 function infiniteLoop(): never { while (true) { } }
PS:类型断言:如果你很清楚一个变量比它现有类型更确切的类型,那么你可以使用类型断言。
类型断言有两种形式:
1.尖括号写法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.As写法:
let someValueTwo: any = "this is a string";
let strLengthTwo: number = (someValueTwo as string).length;
当在TypeScript里使用JSX时,只能使用As语法断言。
2、接口
TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名定义契约。
写法:
interface 接口名 { attribute: type }
示例:
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); }
1.可选属性
interface SquareConfig { color?: string; width?: number; }
2.只读属性
interface Point {
readonly x: number;
readonly y: number;
}
3.只读数组
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = a; ro[0] = 12; // error! ro.push(5); // error! ro.length = 100; // error! a = ro; // error! // a = ro as number[]; 用断言修改数组为可修改!
4.跳过额外的属性检查
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { return { color: 'blue', area:23 } // ... } (方法1) let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); (方法2)索引签名 interface SquareConfig { color?: string; width?: number; [propName: string]: any; } (方法3)将这个对象赋值给一个另一个变量: 因为squareOptions不会经过额外属性检查 let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
5.通过接口定义函数类型
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { let result = source.search(subString); return result > -1; } // or // mySearch = function(src: string, sub: string): boolean { // let result = src.search(sub); // return result > -1; // }
6.可索引的类型
TypeScript支持两种索引签名:字符串和数字。
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0]; // 定义的StringArray接口,它具有索引签名,表示当用number去索引StringArray时会得到string类型的返回值。 interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number类型 name: string // 错误,`name`的类型与索引类型返回值的类型不匹配 } // 将索引签名设置为只读 interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // error!
7.实现接口
TypeScript也能够用它来明确的强制一个类去符合某种契约
interface ClockInterface { currentTime: Date; setTime(d: Date); } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }
类静态部分与实例部分的区别
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor { new (hour: number, minute: number); } class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { } } // 这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。 // 因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。 interface ClockConstructor { new (hour: number, minute: number): ClockInterface; } interface ClockInterface { tick(); } function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface { return new ctor(hour, minute); } class DigitalClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("beep beep"); } } class AnalogClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("tick tock"); } } let digital = createClock(DigitalClock, 12, 17); let analog = createClock(AnalogClock, 7, 32); // 因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。
8.继承接口
和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape { color: string; } interface Square extends Shape { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10;
继承多个接口:
interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
9.混合类型
一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter = <Counter>function (start: number) { console.log(start) }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
10.接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { select() { } } // 错误:“Image”类型缺少“state”属性。 class Image implements SelectableControl { select() { } } class Location { } // 在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。
因为只有 Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。 // 在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上, SelectableControl接口和拥有select方法的Control类是一样的。
Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的。
3.TS类
从ECMAScript 2015,也就是ES 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。
1.类声明
class CreateClass { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } let greeter = new CreateClass('demo');
2.类继承
2.1类继承:类从基类中继承了属性和方法。这里,Dog是一个派生类,它派生自ParentClass基类,通过extends关键字。派生类通常被称作子类,基类通常被称作超类。
class ParentClass { move(distanceInMeters: number = 0) { console.log(`Animal moved ${distanceInMeters}m.`); } } class Dog extends ParentClass { bark() { console.log('Woof! Woof!'); } } const dog = new Dog(); dog.bark(); dog.move(10); dog.bark();
2.2类私有属性:
class Animal { private name: string; constructor(theName: string) { this.name = theName; } } class Rhino extends Animal { constructor() { super("Rhino"); } } class Employee1 { private name: string; constructor(theName: string) { this.name = theName; } } let animal = new Animal("Goat"); let rhino = new Rhino(); let employee = new Employee1("Bob"); console.log(animal.name); // 错误
2.3类受保护属性:
class Person { protected name: string; constructor(name: string) { this.name = name; } } class Employee extends Person { private department: string; constructor(name: string, department: string) { super(name) this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; } } let howard = new Employee("Howard", "Sales"); console.log(howard.getElevatorPitch()); console.log(howard.name); // 错误
构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如:
class Person2 { protected name: string; protected constructor(theName: string) { this.name = theName; } } // Employee 能够继承 Person class Employee2 extends Person { private department: string; constructor(name: string, department: string) { super(name); this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; } } let howard2 = new Employee2("Howard", "Sales"); let john = new Person2("John"); // 错误: 'Person' 的构造函数是被保护的.
2.4静态属性:类的静态成员,这些属性存在于类本身上面而不是类的实例上。
class Grid { static origin = {x: 0, y: 0}; calculateDistanceFromOrigin(point: {x: number; y: number;}) { let xDist = (point.x - Grid.origin.x); let yDist = (point.y - Grid.origin.y); return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale; } constructor (public scale: number) { } } let grid1 = new Grid(1.0); // 1x scale let grid2 = new Grid(5.0); // 5x scale console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10})); console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
2.5抽象类:抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。
abstract class AbstractClass { abstract makeSound(): void; move(): void { console.log('roaming the earch...'); } }
抽象类中的抽象方法不包含具体实现且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。
abstract class Department { constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; // 必须在派生类中实现 } class AccountingDepartment extends Department { constructor() { super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super() } printMeeting(): void { console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports...'); } } let department: Department; // 允许创建一个对抽象类型的引用 department = new Department(); // 错误: 不能创建一个抽象类的实例 department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值 department.printName(); department.printMeeting(); department.generateReports(); // 错误: 方法在声明的抽象类中不存在
2.6类当做接口使用
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
4.泛型(generic)
function identity1<T>(arg: T): T { return arg; }
1.泛型类型:与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity<T>(arg: T): T { return arg; } let myIdentity: <T>(arg: T) => T = identity; // or let myIdentity1: {<T>(arg: T): T} = identity; // 这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口 interface GenericIdentityFn { <T>(arg: T): T; } function identity<T>(arg: T): T { return arg; } let myIdentity: GenericIdentityFn = identity;
2.泛型类
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; }; // GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。 let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; }; console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
5.枚举
1.数字枚举
enum Direction { Up = 1, // 使用初始值,递增,否则默认从0开始 Down, Left, Right }
2.字符串枚举
enum Direction2 { Up = "UP", // 每个字符串枚举成员必须进行初始化 Down = "DOWN", Left = "LEFT", Right = "RIGHT", }
3.异构枚举(可以混合字符串和数字成员)
例1:
enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES", }
例2:
enum E { Foo, Bar, } function f(x: E) { if (x !== E.Foo || x !== E.Bar) { // ~~~~~~~~~~~ // Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'. } }
4.联合枚举与枚举成员的类型
enum ShapeKind { Circle, Square, } interface Circle { kind: ShapeKind.Circle; radius: number; } interface Square { kind: ShapeKind.Square; sideLength: number; } let c11: Circle = { kind: ShapeKind.Square, // 正确的为ShapeKind.Circle // ~~~~~~~~~~~~~~~~ Error! radius: 100, }
5.运行时枚举
枚举是在运行时真正存在的对象
enum E { X, Y, Z } function f(obj: { X: number }) { console.log('X',obj.X); return obj.X; } // Works, since 'E' has a property named 'X' which is a number. f(E);
6.反向映射
除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了反向映射
enum Enum { A } let a = Enum.A; let nameOfA = Enum[a]; // "A" // 生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。 // 要注意的是 不会为字符串枚举成员生成反向映射,因为枚举成员不能具有数值名,所以数字枚举成员具有反射
7.常量(const)枚举
为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义。
常量枚举注意点:
1.不会生成反向映射
2.不能直接访问值
const enum Order {
A,
B,
C,
}
8.外部枚举
外部枚举用来描述已经存在的枚举类型的形状,简单理解就是方便用户编写函数时的提示
declare enum Enum { A = 1, B, C = 2 }
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
用来描述一个应该存在的枚举类型的,而不是已经存在的,它的值在编译时不存在,只有等到运行时才知道。
6.模块
TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。
1.导出
变量,函数,类,类型别名或接口都可以通过export导出
导出声明
export interface StringValidator { isAcceptable(s: string): boolean; } export const numberRegexp = /^[0-9]+$/; export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } }
导出语句
class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } } export { ZipCodeValidator }; export { ZipCodeValidator as mainValidator };
7.高级类型
1.交叉类型:多个类型合并为一个类型
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 Person1 { constructor(public name: string) { } } interface Loggable { log(): void; } class ConsoleLogger implements Loggable { log() { // ... return 11; } } var jim = extend(new Person1("Jim"), new ConsoleLogger()); var n1 = jim.name; jim.log();
2.联合类型
联合类型表示一个值可以是几种类型之一。用竖线( | )分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。
function padLeft(value: string, padding: string | number | boolean) { // ... } let indentedString = padLeft("Hello world", true); // 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。 interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors
3.类型保护
let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); }
1.自定义类型保护
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; } // 'swim' 和 'fly' 调用都没有问题了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
2.typeof类型保护
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}'.`); }
3.instanceof类型保护
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' }
4.可以为null的类型
let s = "foo"; s = null; // 错误, 'null'不能赋值给'string' let sn: string | null = "bar"; sn = null; // 可以 sn = undefined; // error, 'undefined'不能赋值给'string | null'
5.可选参数和可选属性
使用了 --strictNullChecks,可选参数会被自动地加上 | undefined。
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' // 可选属性也会有同样的处理: 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'
6.类型断言
可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。
如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined:
function broken(name: string | null): string { function postfix(epithet: string) { return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null } name = name || "Bob"; return postfix("great"); } function fixed(name: string | null): string { function postfix(epithet: string) { return name!.charAt(0) + '. the ' + epithet; // ok } name = name || "Bob"; return postfix("great"); }
本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name的类型。
7.类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } } // 起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。 // 同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入: type Container<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
8.接口 & 类型别名
接口创建了一个新的名字,可以在其它任何地方使用。类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。
type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface;
类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
9.字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。
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 // 字符串字面量类型还可以用于区分函数重载: function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; function createElement(tagName: string): Element { }
10.数字字面量类型
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { // ... return 1; } function foo(x: number) { if (x !== 1 || x !== 2) { // ~~~~~~~ // Operator '!==' cannot be applied to types '1' and '2'. } }
11.可辨识联合
你可以合并单例类型、联合类型、类型保护和类型别名来创建一个叫做【可辨识联合的高级模式】,它也称做【标签联合】或【代数数据类型】。可辨识联合在函数式编程很有用处。一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。它具有3个要素:
- 具有普通的单例类型属性 — 可辨识的特征。
- 一个类型别名包含了那些类型的联合 — 联合。
- 此属性上的类型保护。
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
首先我们声明了将要联合的接口。每个接口都有kind属性但有不同的字符串字面量类型。kind属性称做可辨识的特征或标签。其它的属性则特定于各个接口。注意,目前各个接口间是没有联系的。下面我们把它们联合到一起:
type Shape = Square | Rectangle | Circle; // 现在我们使用可辨识联合: 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; } }
12.完整性约束
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle到 Shape,我们同时还需要更新 area:
type Shape = Square | Rectangle | Circle | Triangle; 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; }
请发表评论