一、 对象
Typescript 中 Object 类型不单是指普通对象类型,它泛指所有的非原始类型,也就是对象,数组还有函数。
普通对象就是键值对的集合,我们可以使用接口来定义对象的结构。
interface Person { // Person是接口CHname: string;age: number;className: string;}let personInfo: Person = {CHname: "迪西",age: 10,className: '三年级',};console.log(personInfo); // {CHname: '迪西', age: 10, className: '三年级'}
- 对象解构赋值
TypeScript 支持对象解构赋值,让我们能够更便捷地从对象中提取值并赋给变量。let {age, CHname } = personInfo; // 将age和CHname 属性,分别复制给age和CHname console.log(CHname ); // 迪西 console.log(age); // 10
- 对象展开运算符
对象展开运算符 (…) 允许我们将一个对象的属性展开到另一个对象中,并创建一个新的对象。
let addressInfo = {city: '天津',schoolName: '第三实验小学', }let info = {...personInfo, ...addressInfo}; console.log(info); // {CHname: '迪西', age: 10, className: '三年级', city: '天津', schoolName: '第三实验小学'} // 用展开运算符 (...)将personInfo对象和addressInfo对象的属性添加到info 对象中,info 未新对象
- 对象类型断言
有时候需要告诉 TypeScript 某个对象的类型,可以使用类型断言。let unknownPerson: any = { CHname: "丁丁", age: 12 }; let typedPersonInfo = unknownPerson as Person; console.log(typedPersonInfo); // {CHname: '丁丁', age: 12
- 深层对象属性
当对象包含嵌套结构时,通过点操作符,我们可以访问深层对象的属性。interface Person {CHname: string;age?: number;className?: string;address: Address; }interface Address {cityCHname: string;schoolName: string; }let personInfo: Person = {CHname: "迪西",address: { cityCHname: "天津", schoolName: "第三实验小学" }, }; console.log(personInfo.address.cityCHname); // 天津
- 对象的方法
方法是与对象相关联的函数。interface Person {CHname: string;age?: number;className?: string;bark(): void; } let personInfo: Person = {CHname: "迪西",bark(){console.log('Hello')} }; personInfo.bark(); // Hello
二、 接口
可以理解为一种规范或者契约,它可以用来约定对象的结构,我们去使用一个接口就要去遵循这个接口的所有约定。在 typescript 中,接口最直观的体现是,约定一个对象当中具体应该有那些成员,并且这些成员是什么类型的。
编译之后,没有任何有关接口的代码,其实 typescript 的接口只是为我们**有结构的数据做类型约束,在运行阶段这种接口没有意义**。
-
基本用法
接口是一种抽象的数据类型,定义了对象的结构,包括属性类型和方法。interface Person {CHname: string;age: number; }function greet(person: Person): string {return `${person.CHname} ${person.age}`; } console.log(greet({CHname: '迪西', age: 10})); // 迪西 10
编译之后
function greet(person) {return "".concat(person.CHname, " ").concat(person.age); } console.log(greet({ CHname: '迪西', age: 10 }));
-
可选属性
在 TypeScript 中,我们可以通过在属性后面加上 ? 来实现,接口中的某些属性是可选的。interface User {name: string;age?: number; } let userInfo1: User = { name: "丁丁" }; // age 是可选的,可以选择在对象赋值时选择添加或者省略。 let userInfo2: User = { name: "小波", age: 9 };
-
只读属性
初始化过后不能再修改,关键字 readonlyinterface Post {title: stringcontent: stringsubtitle?: string // 可选成员readonly summary: string // 只读成员 } const hello: Post = {title: 'hello, typeScript',content: 'a javascript superset',summary: 'a javascript', } hello.summary = 'typeScript';
-
可索引类型的接口
接口还可以描述具有索引类型的对象,使我们能够按索引访问对象的属性。// StudentArray 表示具有数字索引的字符串数组, 通过索引访问数组元素。 interface StudentArray {[index: number]: string; } let myClass: StudentArray; myClass = ["迪西", "丁丁"];let student: string = myClass[0]; console.log(student); // 迪西
-
接口继承
接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
继承关键字:extends,如果继承多个用“逗号”隔开interface Shape {color: string; } interface Square extends Shape { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10; console.log(square); // {color: 'blue', 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; console.log(square); // {color: 'blue', sideLength: 10, penWidth: 5}
-
混合类型
接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型interface Counter {(start: number): string;interval: number;reset(): void; }function getCounter(): Counter {let counter = <Counter>function (start: number) { };counter.interval = 123;counter.reset = function () { };return counter; }let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
interface Counter {(start: number, end: number): string; }function getCounter(): Counter {let counter = <Counter>function (start: number, end: number) {console.log(start + end); // 12};return counter } let c = getCounter(); c(10, 2);
-
类实现接口
TypeScript也能够用它来明确的强制一个类去符合某种契约。
注:接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
关键字 implementsinterface ClockInterface {currentTime: Date;setTime(d: Date): void;}class Clock implements ClockInterface {currentTime: Date = new Date();setTime(nDate: Date): void {this.currentTime = nDate;}}let clock = new Clock();clock.setTime(new Date());console.log(clock.currentTime); // Tue Mar 05 2024 10:57:48 GMT+0800 (中国标准时间)
-
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任
何关系。class Control {private state: any; }interface SelectableControl extends Control {select(): void; } // SelectableControl包含了Control的所有成员,包括私有成员state。 因为 state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口,因为只有 Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。 class Button extends Control implements SelectableControl {select() { } }class TextBox extends Control {select() { } }// 错误:“Image”类型缺少“state”属性。 class Image implements SelectableControl {select() { } }class Location {} // 在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上, SelectableControl接口和拥有select方法的Control类是一样的。 //Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法), //但Image和Location类并不是这样的。
三、类
类,面向对象编程的核心,涵盖了类的基本定义、继承、访问修饰符、静态成员与静态方法、类的抽象等方面
使用 class 关键字来定义类。
class Greeter {greeting: string;constructor(message: string) {this.greeting = message;}greet() {return "Hello, " + this.greeting; // this表示是访问的是类的成员。}}let greeter = new Greeter("world");console.log(greeter); // { greeting: "world" }
类的使用注意点:
- 直接使用 this 去访问当前类的属性会报错,是因为在 TypeScript 当中,我们要明确在类型当中去声明它所拥有的一些属性,而不是直接在构造函数当中通过 this 动态添加。
class Person {constructor(name: string, age: number) {this.name = name; // 报错this.age = age; // 报错} }
- 在 typescript 当中,类的属性必须要有一个初始值,要么在声明的时候通过=号去赋值,要么就在构造函数里初始化。
class Person {name: string; // 报错age: number; // 报错constructor(name: string, age: number) {// this.name = name;// this.age = age;} }
-
类的访问修饰符
public: 默认为 publicclass Animal {public name: string;public constructor(theName: string) { this.name = theName; }public move(distanceInMeters: number) {console.log(`${this.name} moved ${distanceInMeters}m.`);} }
private:不能在声明它的类的外部访问
class Animal {private name: string;constructor(theName: string) { this.name = theName; } }new Animal("Cat").name; // 错误: 'name' 是私有的.
带有 private或 protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则。class Animal {private name: string;constructor(theName: string) { this.name = theName; } }class Rhino extends Animal {constructor() { super("Rhino"); } } // Rhino是Animal的子类 Animal和 Rhino共享了来自 Animal里的私有成员定义 private name: string,因此它们是兼容的 class Employee {private name: string;constructor(theName: string) { this.name = theName; } } // Employee尽管和Animal很像也有私有成员name,但和Animal无关let animal = new Animal("Goat"); let rhino = new Rhino(); let employee = new Employee("Bob");animal = rhino; animal = employee; // 错误: Animal 与 Employee 不兼容.
protected:派生类中仍然可以访问class Person {protected name: string;constructor(name: string) { this.name = name; } }class Employee extends Person {private age: number;constructor(name: string, age: number) {super(name)this.age = age;}public getElevatorPitch() {return `name: ${this.name}; age ${this.age}.`;} } let howard = new Employee("迪西", 9); console.log(howard.getElevatorPitch()); console.log(howard.name); // 错误
构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承class Person {protected name: string;protected constructor(theName: string) { this.name = theName; } }// Employee 能够继承 Person class Employee extends Person {private age: number;constructor(name: string, age: number) {super(name);this.age = age;}public getElevatorPitch() {return `Hello, my name is ${this.name} and I work in ${this.age}.`;} } let howard = new Employee("迪西", 9); let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.
readonly: 只读属性必须在声明时或构造函数里被初始化。class Octopus {readonly name: string;readonly numberOfLegs: number = 8;constructor (theName: string) {this.name = theName;} } let dad = new Octopus("Man with the 8 strong legs"); dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
-
类的构造函数参数属性
可以在构造函数的参数前面添加访问修饰符,它将自动变成类的成员。class Person {constructor(private name: string, public age: number) {} } // private 关键字,它将 name 参数自动转换为类的私有成员 // public 关键字,它将 age 参数转换为类的公共成员 // 相当于在类中声明了 private name: string ,和 public age: number。
-
参数属性的默认值
可以为构造函数的参数提供默认值,避免在类外部手动初始化。class Car {constructor(public brand: string, public model: string = "BaseModel") {console.log(brand, model); // 比亚迪 BaseModel} } let carData = new Car('比亚迪')
-
存取器(Getter 和 Setter)
使用 get 和 set 关键字,可以定义存取器,允许你在访问或设置属性值时执行额外的逻辑class Person {private name: string = '';private age: number = 0;get getAge(): number {return this.age;}set setAge(value: number) {if (value < 6) {throw new Error(this.name + "年龄太小了!");}this.age = value;} }
-
类的继承
继承是面向对象编程中的重要概念,在 TypeScript 中,我们可以使用 extends 关键字实现类的继承。class Animal {move(distanceInMeters: number = 0) {console.log(`Animal moved ${distanceInMeters}m.`);} } class Dog extends Animal {bark() {console.log('Woof! Woof!');} } const dog = new Dog(); dog.bark(); dog.move(10); dog.bark(); // Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。
以上代码是基础用法,类从基类中继承了属性和方法
class Animal {name: string;constructor(theName: string) { this.name = theName; }move(distanceInMeters: number = 0) {console.log(`${this.name} moved ${distanceInMeters}m.`);} }class Snake extends Animal {constructor(name: string) { super(name); }move(distanceInMeters = 5) {console.log("Slithering...");super.move(distanceInMeters);} }class Horse extends Animal {constructor(name: string) { super(name); }move(distanceInMeters = 45) {console.log("Galloping...");super.move(distanceInMeters);} }let sam = new Snake("Sammy the Python"); let tom: Animal = new Horse("Tommy the Palomino");sam.move(); tom.move(34); // 打印结果 // Slithering... // Sammy the Python moved 5m. // Galloping... // Tommy the Palomino moved 34m. // 使用 extends关键字创建了 Animal的两个子类: Horse和 Snake。派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数
TypeScript强制执行的一条重要规则。在构造函数里访问 this的属性之前,我们 一定要调用 super()
-
静态成员与静态方法
静态成员和静态方法属于类本身,而不是类的实例。它们可以通过类名直接访问,而无需实例化类,适用于不依赖于实例状态的情况。
使用关键字:staticclass 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 scaleconsole.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10})); console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10})); // 用 static定义 origin, 每个实例想要访问这个属性的时候,都要在 origin前面加上类名。 // 如同在实例属性上使用 this.前缀来访问属性一样,这里我们使用 Grid.来访问静态属性。
-
类的抽象
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。abstract class Animal {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(); // 错误: 方法在声明的抽象类中不存在
-
抽象静态成员
可以在抽象类中定义静态成员,派生类可以继承这些静态成员。abstract class Animal {static type: string;abstract makeSound(): void; }class Dog extends Animal {static type = "Dog";makeSound(): void {console.log("Woof!");} }
-
类与接口
类与类之间的一些共同点可以用接口去抽象,interface EatAndRUn {eat (food: string) : voidrun (distance: number) : void } // 通过关键字 implements 实现 这个 EatAndRUn 接口 // 此时这个类必须要有这个接口对应的成员 class Person implements EatAndRUn{eat (food: string): void {console.log(`优雅的进餐:${food}`)}run (distance: number) {console.log(`直立行走:${distance}`)} }class Animal implements EatAndRUn{eat (food: string): void {console.log(`呼噜呼噜的吃:${food}`)}run (distance: number) {console.log(`爬行:${distance}`)} }
需要注意的是:在 C# 和 Java 这些语言当中,它建议我们尽可能让每个接口的定义更加简单更加细化,因此我们建议一个接口只去约束一个能力,让一个类同时实现多个接口。
interface Eat {eat (food: string): void }interface Run {run (distance: number): void }class Person implements Eat, Run {eat (food: string): void {console.log(`优雅的进餐: ${food}`)}run (distance: number) {console.log(`直立行走: ${distance}`)} }class Animal implements Eat, Run {eat (food: string): void {console.log(`呼噜呼噜的吃: ${food}`)}run (distance: number) {console.log(`爬行: ${distance}`)} }
四、泛型
-
泛型基础
指的是我们去定义函数,接口,类的时候没有去定义具体类型,我们在使用的时候再去定义指定类型的这一种特征。
能通过泛型编写更通用的代码,摆脱对数据类型的限制。
泛型就是在我们定义的时候把不明确的类型,变成一个参数,在我们使用的时候再传递这样的一个类型参数。function identity<T>(arg: T): T {return arg; } let output1 = identity<string>("myString"); let output2 = identity<number>(9);
-
泛型数组
Array 是一个泛型类,在 typescript 中去定义这个 Array 类型时,它不知道我们使用它去存放什么样类型的数据,所以它就使用泛型参数,在我们去调用的时候再传入具体类型,这是一个泛型提现。function printArray<T>(arr: T[]): void {for (let item of arr) {console.log(item); // 1 2 3 4 5 迪西 丁丁 拉拉 小波} } let numbers: number[] = [1, 2, 3, 4, 5]; printArray(numbers);let nameList: string[] = ["迪西", "丁丁", "拉拉", "小波"]; printArray(nameList);
-
泛型约束
对泛型进行一些限制,确保传入的类型拥有某些属性或方法,这就是泛型约束的作用let nameList: string[] = ["迪西", "丁丁", "拉拉", "小波"]; interface Lengthwise {length: number; }function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length); //nameList数组的长度 4 传入对象属性length的长度 9return arg; } // loggingIdentity(3); // 传入的数字没有 length属性 loggingIdentity(nameList); loggingIdentity({length: 9, value: 3});
-
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。
类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。class Box<T> {private content: T;constructor(initialContent: T) {console.log(initialContent); // 42 TypeScriptthis.content = initialContent;}getContent(): T {return this.content;} } let numberBox = new Box<number>(42); console.log(numberBox.getContent()); // 42 TypeScriptlet stringBox = new Box<string>("TypeScript"); console.log(stringBox.getContent());
-
泛型与函数重载
泛型与函数重载结合使用,可以更灵活地处理不同类型的输入。function getResult(arg: string): string; function getResult(arg: number): number; function getResult<T>(arg: T): T {return arg; }let resultString: string = getResult("TypeScript"); let resultNumber: number = getResult(18);console.log(resultString); // TypeScript console.log(resultNumber); // 18