本节介绍ts的接口及类相关内容,接口是ts中为类型或第三方代码定义契约,有时被称做“鸭式辨型法”或“结构性子类型化”。
Ts是需要对变量等指定类型并进行类型检查,定义方式如下:
interface In{
a: string;
}
function printA(obj: In) {
console.log(obj.a);
}
let myObj = {size: 10, a: "Size 10 Object"};
printA(myObj);
类型检查器会查看printA的调用,printA有一个参数,并要求参数必须有一个类型为string的a变量,编译器只会检查那些必须的属性是否存在及类型是否匹配,多余的属性并不会被检测,入上面的例子中传输的myObj中除了a属性还有size属性,但并不会影响参数的传递。
接口就好比一个名字,用来描述了类型的具体结构,上述例子代表了一个类型为string的a属性,这里并不是说myObj实现了In接口,只要传入的对象满足对应的要求,就被允许。
类型检查器不会检查属性的顺序,只要属性存在并且类型对应就可以。
接口中的属性有些有时候不是必需的,或者在某些情况下需要,这时候就需要使用可选属性的接口,定义方式和普通的接口定义差不多,只是可选属性名字定义的后面加一个?即可,示例如下:
interface Config{
color?: string;
width?: number;
}
function create(config: Config): {color: string; area: number} {
let obj= {color: "white", area: 100};
if (config.color) {
obj.color = config.color;
}
if (config.width) {
obj.area = config.width * config.width;
}
return obj;
}
let mySquare = create({color: "black"});
可选属性的好处是可以对可能存在的属性进行预定义,也可以捕获引用了不存在的属性时的错误,如属性名若写错误,将会得到一个错误提示:
interface Config{
color?: string;
width?: number;
}
function create(config: Config): {color: string; area: number} {
let obj= {color: "white", area: 100};
if (config.color) {
obj.color = config.colorr;//此处将提示属性异常
}
if (config.width) {
obj.area = config.width * config.width;
}
return obj;
}
let mySquare = create({color: "black"});
只读属性是只有在创建时设置其值,之后将不可修改,在属性前面添加readonly进行指定,定义方式如下:
interface P{
readonly x: number;
readonly y: number;
}
创建对象时,给x和y进行赋值,赋值后将不可修改,如:
let p1: P= { x: 10, y: 20 };
p1.x = 5; // 此处将会保存,因为x属性是只读的。
TS中有特殊的只读数组,定义方式为:ReadonlyArray类型,它与Array相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // 此处将报错
ro.push(5); // 此处将报错
ro.length = 100; // 此处将报错
a = ro; // 此处将报错
可以使用类型断言进行重写:
a = ro as number[];
readonly和const的区别:主要判断是用作变量还是属性,作为变量是使用const,作为属性时使用readonly。
interface Config {
color?: string;
width?: number;
}
function create(config: Config ): { color: string; area: number } {
// ...
}
let mySquare = create({ colour: "red", width: 100 });
此实例中传输的参数中color属性拼写为了colour,这种按照可选属性的写法是可以,但是实际情况是会报错,因为对象的字面量会被特殊对待,而且会经过额外属性检查,即当将它们赋值给变量或作为参数传递的时候,如果一个对象字面量存在任何目标类型不包含的属性时,将会报错。
// 此处将报错,因为Config中没有colour的属性。
let mySquare = create({ colour: "red", width: 100 });
需要绕开的话,**第一种方式是:**可以使用类型断言进行处理:
let mySquare = create({ width: 100, opacity: 0.5 } as Config);
**第二种方式是:**若能确定这个对象可能具有某些作为特殊用途使用的额外属性,最佳的方式是添加一个字符串索引签名,如下:
interface Config {
color?: string;
width?: number;
[propName: string]: any;
}
上述例子中表示Config可以有任意数量的属性,并且只要名称不是color或width,则其类型无所谓。
**第三种方式是:**可以将对象赋值给另一个变量,因为另一个变量不会经过额外属性检查,所以可以绕开,实例如下:
let options = { colour: "red", width: 100 };
let mySquare = create(options );
接口能够描述js中对象的外形,除了描述带有属性的普通对象外,也可以描述函数的类型,定义方式如下:
interface Fun{
(a: string, b: string): boolean;
}
定义后,可以像使用其他接口一样使用这个函数类型的接口,定义方式如下:
let mySearch: Fun;
mySearch = function(a: string, b: string) {
let result = a.search(b);
if (result == -1) {
return false;
}
else {
return true;
}
}
函数中参数的名称可以和接口中定义的名称不一致,如:
let mySearch: Fun;
mySearch = function(c: string, d: string): boolean {
let result = c.search(d);
if (result == -1) {
return false;
}
else {
return true;
}
}
函数的参数检查时,会逐个进行,要求对应位置上的类型是兼容的,若不指定类型,ts会自动推断出参数的类型,示例如下:
let mySearch: Fun;
mySearch = function(a, b) {
let result = a.search(b);
if (result == -1) {
return 1;
}
else {
return 2;
}
}
上述示例中将会警告函数的返回值与接口中的定义不匹配。
可以描述能够通过索引得到的类型,如a[10]或a[‘b’],可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型,如下:
interface Test{
[index: number]: string;
}
let myArray: Test;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上述示例中Test具有索引签名,描述了使用number类型去索引Test时会得到string类型的返回值。
只支持字符串和数字两种索引签名,可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。因为使用number来索引时,js会将它转换成string然后再去索引对象,示例如下:
class A{
name: string;
}
class B extends A{
breed: string;
}
// 此处将报错,因为索引必须时number或string的,不能使用其他类型。
interface NotOkay {
[x: number]: A;
[x: string]: B;
}
字符串索引签名会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property和obj[“property”]两种形式。下面示例中name的类型与字符串索引类型不匹配,会得到异常提示。
interface A{
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型不是索引类型的子类型
}
可以将索引签名设置为只读,这样就防止了给索引赋值:
interface A{
readonly [index: number]: string;
}
let myArray: A= ["Alice", "Bob"];
myArray[2] = "Mallory"; // 此处的索引签名是只读的,所以不能直接设置
实现接口,ts中可以明确的强制一个类去符合某种契约,如下:
interface A{
time: Date;
}
class Clock implements A{
time: Date;
constructor(h: number, m: number) { }
}
也可以在接口中描述一个方法,在类中实现它,示例如下:
interface A{
time: Date;
setTime(d: Date);
}
class Clock implements A{
time: Date;
setTime(d: Date) {
this.time= d;
}
constructor(h: number, m: number) { }
}
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
类具有静态部分类型和实例部分类型,当用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface A{
new (hour: number, minute: number);
}
class Clock implements A{
currentTime: Date;
constructor(h: number, m: number) { }
}
当一个类实现了一个接口时,只对其实例部分进行类型检查。 构造函数constructor存在于类的静态部分,所以不在检查的范围内。若需要检测构造函数中参数,可使用以下方式:
interface AConstructor {
new (hour: number, minute: number): AInterface ;
}
interface AInterface {
tick();
}
function create(ctor: AConstructor , hour: number, minute: number): AInterface {
return new ctor(hour, minute);
}
class B implements AInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class C implements AInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = create(B, 12, 17);
let analog = create(C, 7, 32);
上述示例中create的第一个参数是AConstructor 类型,在create里,会检查参数是否符合构造函数签名。
接口可以相互扩展,可以将属性从一个接口复制到另一个接口,如下:
interface A{
color: string;
}
interface B extends A{
sideLength: number;
}
let square = <A>{};
square.color = "blue";
square.sideLength = 10;
一个接口也可以继承多个接口,可创建出多个接口的合成接口。
interface A{
color: string;
}
interface B{
penWidth: number;
}
interface C extends A, B{
sideLength: number;
}
let square = <A>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
一个接口可以同时作为函数和对象使用,并带有额外的属性,在使用js第三方库的时候,也可以完整的定义对应的类型,如下:
interface C{
(start: number): string;//函数
interval: number;//属性
reset(): void;//函数
}
function getC(): C{
let counter = <C>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getC();
c(10);
c.reset();
c.interval = 5.0;
当接口继承了一个类类型时,会继承类的成员,但不包括函数的具体实现,类似于声明了类中的所有成员,但并没有提供具体的实现。
接口也会继承类的private和protected成员,即当创建了一个接口,此接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类实现。
class C{
private state: any;
}
interface D extends C{
select(): void;
}
class B extends C implements D{
select() { }
}
class T extends C{
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class E implements D{
select() { }
}
上述示例中接口D包含了C的所有成员,包括私有成员state,因为state是私有成员,所以只有C的子类才能实现D接口,因为只有C的子类才能够拥有一个声明与C的私有成员state。在C类内部,允许通过D的实例来访问私有成员state的,D就像C一样,并拥有一个select方法,B和T类是D的子类,所以可以正常定义,但E类并没有继承C,所以会报错。
JS使用函数和基于原型的继承来创建可重用的组件,从ES6开始,js也能够使用基于类的面向对象的方式。定义方式如下:
class C{
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new C("world");
引用类成员的时候需要使用this,表示访问的是类的成员。
可以使用继承来扩展现有的类,使用方式如下:
class A{
name:string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class B extends A{
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class C extends A{
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new B("Sammy the Python");
let tom: A= new C("Tommy the Palomino");
sam.move();
tom.move(34);
使用extends关键字进行继承定义,子类可以访问父类的属性和方法,子类的构造函数中必须调用super(),会执行基于基类的构造方法,子类也可以重写父类的方法。
修饰符包括公共/私有和受保护的类型,分别是public/private/protected。
public修饰符:
ts中默认的修饰符是public,也可以明确的将一个成员标记成public,实例如下:
class A{
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
private修饰符:
当成员被标记成private时,就不能在声明它的类的外部访问,示例如下:
class A{
private name: string;
constructor(theName: string) { this.name = theName; }
}
new A("Cat").name; //此处将报错,因为name属性时私有的,所以不可以访问。ts中比较两种不同的类型时,并不在乎它们从何处而来,如果所有的类型都是兼容的,就认为他们的类型是兼容的。
比较带有private和protected成员类型的时候,如果一个类型里包含一个private成员,那么只有当另一个类型中也存在这样的一个private成员,并且它们都来自同一处声明时,才认为这两个类型是兼容的,protected修饰符也是同样的规则,示例如下:
class A{
private name: string;
constructor(theName: string) { this.name = theName; }
}
class B extends A{
constructor() { super("B"); }
}
class C{
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new A("Goat");
let rhino = new B();
let employee = new C("Bob");
animal = rhino;
animal = employee; // 此处将会报错,因为C类没有继承A,虽然有相同的private属性,但是它们的声明却不是同一个地方,所以是不兼容的。
protected修饰符:
protected修饰符和private修饰符类似,不同的地方就是protected成员在派生类中任然可以访问,示例如下:
class A{
protected name: string;
constructor(name: string) { this.name = name; }
}
class B extends A{
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 B("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 此处将报错,因为name是私有成员
不能在A类外使用name,但可以使用B的实例方法访问,因为B是A派生而来的。构造函数也可以被标记为protected,标记后这个类将不能在包含它的类外被实例化,但是能被继承,如:
class A{
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// 此处能继承
class B extends A{
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 B("Howard", "Sales");
let john = new A("John"); // 此处将报错,因为A的构造函数是受保护的,不能在其外部访问。
readonly修饰符
readonly修饰符能将属性设置为只读的,设置后此属性必须在声明时或构造函数里被初始化,其他地方无法进行修改值,示例如下:
class O{
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new O("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 此处将报错,因为属性name是只读的,所以无法进行重新赋值。
定义成员属性也可以通过参数属性的方式简写,参数属性可以在一个地方定义并初始化一个成员,示例如下:
class A{
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
上述示例中省略了属性的定义及构造函数中赋值的操作,将声明及赋值合并到了一起,参数属性通过给构造函数中参数添加一个访问限定符进行声明。
ts支持通过getters/setters来截取对象成员的访问,能有效的控制对对象成员的访问,p普通使用方式如下:
class A{
fullName: string;
}
let a= new A();
a.fullName = "Bob Smith";
if (a.fullName) {
console.log(a.fullName);
}
可以将上述示例改写成get/set方式进行处理,示例如下:
let a= "secret passcode";
class A{
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let a= new A();
a.fullName = "Bob Smith";
if (a.fullName) {
alert(a.fullName);
}
存取器要求编辑器的输出设置为ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有set的存取器自动被推断为readonly。
静态属性是存在与类本身而不是类的实例上,而实例成员是当类被实例化的时候才会被初始化的属性。静态属性使用static进行声明,访问时必须加上对应的类名才能进行访问,实例属性上使用this.来访问属性,实例如下:
class A{
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 A(1.0); // 1x scale
let grid2 = new A(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
抽象类是为其派生类的基类使用,不会被实例化,不同于接口,抽象类可以包含成员的实现细节,关键字是abstract,可以用于定义抽象类和抽象类内部抽象方法。
abstract class A{
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
抽象类中的抽象方法不能包含具体的实现,而且需要在派生类中实现具体的内容,语法和接口方法类似,都是包含方法签名但不好含方法实现,抽象方法必须使用abstract 进行声明,实例如下:
abstract class A{
constructor(public name: string) {
}
printName(): void {
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void; // 必须在派生类中实现
}
class B extends A{
constructor() {
super('Accounting and Auditing'); // constructors in derived classes must call super()
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.');
}
generateReports(): void {
console.log('Generating accounting reports...');
}
}
let department: A; // ok to create a reference to an abstract type
department = new A(); // error: 抽象类不能被直接初始化
department = new B(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: 抽象类中的方法不存在
ts声明一个类的时候,也就声明了类的实例的类型,示例如下:
class A{
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: A;
greeter = new A("world");
console.log(greeter.greet());
构造函数会在使用new创建类实例的时候被调用。
class A{
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}else {
return Greeter.standardGreeting;
}
}
}
let greeter1: A;
greeter1 = new A();
console.log(greeter1.greet());
let greeterMaker: typeof A= A;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: A= new greeterMaker();
console.log(greeter2.greet());
上述实例中定义了greeterMaker变量,变量的类型是A类的的构造函数,使用typeof A是指取A类的类型,而不是实例的类型,即构造函数的类型,这个类型包含了类的所有静态成员和构造函数,之后可以使用new关键字创建A类的实例。
类会创建实例类型和一个构造函数,因为类可以创建出类型,所以能够允许使用接口的地方使用类:
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};