进阶 · TypeScript 入门教程
- 知识点概述
- 字符串字面量类型
- 元组
- 枚举
- 类
- 类与接口
- 泛型
- 声明合并
- 类型别名
type ... = ...- 使用
type 在创建类型别名 - 类型别名用来给一个类型起个新名字
- 类型别名常与联合类型配合使用
type StringOrNumber = string | number;let myVar: StringOrNumber;// okmyVar = 'Hello';// okmyVar = 123;// errormyVar = true;
type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); }}
- 字面量类型
let a1: 1 | 2// oka1 = 1// oka1 = 2// errora1 = 3type t2 = '1' | '2'let a2: t2// oka2 = '1'// oka2 = '2'// errora2 = 1
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过 new 生成
- 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
- 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
- 在 js 中,类相关的基础知识点回顾
class Animal { name; constructor(name) { this.name = name; } sayHi() { return `My name is ${this.name}`; }}let a = new Animal('Jack');console.log(a.sayHi()); // My name is Jack
class Cat extends Animal { constructor(name) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() }}let c = new Cat('Tom'); // Tomconsole.log(c.sayHi()); // Meow, My name is Tom
class Animal { constructor(name) { this.name = name; } get name() { return 'Jack'; } set name(value) { console.log('setter: ' + value); }}let a = new Animal('Kitty'); // setter: Kittya.name = 'Tom'; // setter: Tomconsole.log(a.name); // Jack
class Animal { static isAnimal(a) { return a instanceof Animal; }}let a = new Animal('Jack');Animal.isAnimal(a); // truea.isAnimal(a); // TypeError: a.isAnimal is not a function
class Animal { name = 'Jack'; constructor() { // ... }}let a = new Animal();console.log(a.name); // Jack
class Animal { static num = 42; constructor() { // ... }}console.log(Animal.num); // 42
- ts 中的 3 种访问修饰符(Access Modifiers)
**public** 公有的,可以在任何地方被访问到,默认所有的属性和方法都是 **public** 的**private** 私有的,不能在声明它的类的外部访问**protected** 受保护的,它和 **private** 类似,区别是它在子类中也是允许被访问的
- 访问修饰符是 ts 中新增的,在最终的编译结果中是不存在的,仅仅是在我们编写 ts 代码时提供一些约束
class Animal { public name; public constructor(name) { this.name = name; }}let a = new Animal("Jack");console.log(a.name); // Jacka.name = "Tom";console.log(a.name); // Tom
class Animal { private name; public constructor(name) { this.name = name; }}let a = new Animal("Jack");// error 属性“name”为私有属性,只能在类“Animal”中访问。console.log(a.name);// error 属性“name”为私有属性,只能在类“Animal”中访问。a.name = "Tom";
// privateclass Animal { private name; public constructor(name) { this.name = name; }}class Cat extends Animal { constructor(name) { super(name); // error 属性“name”为私有属性,只能在类“Animal”中访问。 console.log(this.name); }}// protectedclass Animal { protected name; public constructor(name) { this.name = name; }}class Cat extends Animal { constructor(name) { super(name); // ok console.log(this.name); }}
- 当构造函数修饰为
**private** 时,该类不允许被继承或者实例化 - 当构造函数修饰为
**protected** 时,该类只允许被继承,但是不允许被实例化
// privateclass Animal { public name; private constructor(name) { this.name = name; }}// error 无法扩展类“Animal”。类构造函数标记为私有。class Cat extends Animal { constructor(name) { super(name); }}// error 类“Animal”的构造函数是私有的,仅可在类声明中访问。let a = new Animal("Jack");// protectedclass Animal { public name; protected constructor(name) { this.name = name; }}// okclass Cat extends Animal { constructor(name) { super(name); }}// error 类“Animal”的构造函数是受保护的,仅可在类声明中访问。let a = new Animal("Jack");
- 访问修饰符也可作用于构造函数的参数,等同于在类中定义该属性并给该属性赋值
class Animal { constructor(public name: string) {}}let a = new Animal("Jack");console.log(a.name); // Jack// 等效class Animal { name: string; constructor(name: string) { this.name = name; }}let a = new Animal("Jack");console.log(a.name); // Jack
**readonly** 可以在类中修饰只读成员**readonly** 若和其他访问修饰符同时存在,需要将 **readonly** 写在后面**readonly** 提供了一种简洁的语法来确保类的某些属性在创建实例后不可更改,这有助于增加代码的不变性和可预测性。
class Circle { readonly PI = 3.14159; constructor(public readonly radius: number) {} getArea() { return this.PI * this.radius * this.radius; }}const circle = new Circle(10);console.log(circle.PI) // 3.14159console.log(circle.radius) // 10// error 无法为“PI”赋值,因为它是只读属性。circle.PI = 3.14;
class Point { constructor(readonly x: number, readonly y: number) {}}const point = new Point(5, 10);console.log(point.x) // 5console.log(point.y) // 10// error 无法为“x”赋值,因为它是只读属性。point.x = 20;
- 你可以将
**readonly** 与 **private** 一起使用,这意味着这个属性只能在类内部读取,且不能修改
class Counter { // 在类内部只读 private readonly maxCount = 100; // 在类内部可读可写 private _count = 0; increment() { if (this._count < this.maxCount) { this._count++; } else { console.log("Reached max count!"); } } getCount() { return this._count; }}
- 抽象类(Abstract Class)是 TypeScript(以及其他一些面向对象语言)中的一个重要概念,用于定义基础类的结构和部分实现,而不允许直接实例化。
- 抽象类是为继承而设计的,通常作为其他派生类的基类使用。
- 抽象类可以包含具体成员(已实现的方法和属性)以及抽象成员(没有具体实现的方法和属性)。
- 抽象类中的抽象方法必须被子类实现。
- 抽象类为建立一系列具有某些相似性的类提供了一个基础,使你可以共享某些功能,同时确保派生类遵循某些结构。
- 在 ts 中,使用
**abstract** 来定义抽象类和其中的抽象成员。
abstract class Animal { abstract makeSound(): void; // 抽象方法,没有具体实现 move(): void { // 普通方法,有具体实现 console.log("The animal moves."); }}class Dog extends Animal { makeSound(): void { console.log("Woof!"); }}class Cat extends Animal { makeSound(): void { console.log("Meow!"); }}// error 无法创建抽象类的实例。// let animal = new Animal();let dog = new Dog();dog.makeSound(); // Woof!dog.move(); // The animal moves.let cat = new Cat();cat.makeSound(); // Meow!
- 在 ts 中,类有两个主要角色:
- 工厂角色:当你使用
new ClassName() 这样的语法时,你实际上是在用类作为一个工厂来创建新的实例对象。 - 类型角色:当你在变量、函数参数或函数返回值上使用类名作为类型标注时,你实际上是在用类作为一个类型。这意味着你可以用这个类名来限制赋值给该变量的值的类型、函数参数的类型或函数返回的类型。
class Dog { name: string; constructor(name: string) { this.name = name; } bark(): string { return `${this.name} says woof!`; }}// 1. 使用 Dog 作为工厂创建新实例const myDog = new Dog("Rex");// 2. 使用 Dog 作为类型来限制函数参数的类型function printDogName(d: Dog) { console.log(d.name);}// okprintDogName(myDog); // Rex
- 实现(implements)是面向对象中的一个重要概念。
- 一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用
**implements** 关键字来实现。这个特性大大提高了面向对象的灵活性。 - 在 ts(和很多其他面向对象的编程语言)中,当我们说一个类“实现”一个接口,我们是指这个类遵循了该接口定义的规范或结构。在实际操作中,这意味着类包含了接口中定义的所有属性和方法,并保证了它们的类型正确性。
- 一个类可以实现一个或多个接口。
// 定义一个接口,表示有名字和一个打招呼的功能
interface Greetable {
name: string;
greet(): void;
}
// 定义一个类,它“实现”了上述接口
class Person implements Greetable {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
// 使用
const person = new Person("Alice");
person.greet(); // Hello, my name is Alice.
// 报警接口
interface Alarm {
alert(): void;
}
// 车灯接口
interface Light {
lightOn(): void;
lightOff(): void;
}
// 汽车类实现“报警接口”和“车灯接口”
class Car implements Alarm, Light {
alert() {
console.log("Car alert");
}
lightOn() {
console.log("Car light on");
}
lightOff() {
console.log("Car light off");
}
}
- 常见的面向对象语言中,接口是不能继承类的,但是在 ts 中,类也可以当做一个类型来使用,因此 ts 是允许接口继承类的
- 在接口继承类的时候,也只会继承它的实例属性和实例方法。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = { x: 1, y: 2, z: 3 };
class Point {
/** 静态属性,坐标系原点 */
static origin = new Point(0, 0);
/** 静态方法,计算与原点距离 */
static distanceToOrigin(p: Point) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
/** 实例属性,x 轴的值 */
x: number;
/** 实例属性,y 轴的值 */
y: number;
/** 构造函数 */
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
/** 实例方法,打印此点 */
printPoint() {
console.log(this.x, this.y);
}
}
interface PointInstanceType {
x: number;
y: number;
printPoint(): void;
}
let p1: Point;
let p2: PointInstanceType;
// 上例中最后的类型 Point 和类型 PointInstanceType 是等价的。
- 泛型(Generics)
- 泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
- 泛型提供了一种方法,让代码能够以类型安全的方式在多种数据类型上工作,而不是被限制于一个单一数据类型。
- 泛型可以被视为类型变量,它们用于捕获和重复使用函数或类的参数类型。
- 思考:如何编写一个函数,使其能够接受多种类型的参数,并返回与输入类型相同的类型?
- 在上述描述中,类型是变化的,而非写死的,对于类似这种类型会变化的情况,可以使用泛型来解决
- 泛型其实就是类型变量
- 泛型提供了在编译时检查类型的能力,而不是运行时。
- 通过使用泛型,您可以编写更通用、可重用的函数和类,而不是为每种数据类型重写它们。
- 您可以为泛型定义约束,以确保输入遵循特定的形状或方法。
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString");
// ts 能够推断出 output1: string
let output2 = identity<number>(42);
// ts 能够推断出 output2: number
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
let arr1 = createArray<string>(3, "x"); // ['x', 'x', 'x']
// ts 能够推断出 arr1: string[]
let arr2 = createArray<string>(3, "x"); // ['x', 'x', 'x']
// ts 能够推断出 arr2: string[]
- 当定义泛型时,你可以定义多个类型参数。多个类型参数之间用逗号
, 分隔。
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
const result = swap([7, "seven"]); // ['seven', 7]
// ts 能够推断出 result: [string, number]
- 在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法。这时,你可以通过定义一个接口来约束泛型变量的形状(即它应该有哪些属性或方法)。
// 定义一个接口,约束了泛型 T 必须有 length 属性
interface HasLength {
length: number;
}
function printLength<T extends HasLength>(input: T): void {
console.log(input.length);
// 现在我们可以安全地访问 length 属性,因为我们约束了 T 必须有 length 属性
}
printLength("Hello"); // 输出:5
printLength([1, 2, 3, 4, 5]); // 输出:5
// printLength(123);
// error 类型“number”的参数不能赋给类型“HasLength”的参数。
// 因为 number 类型没有 length 属性
- 多个类型参数之间也可以互相约束对
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
let result = copyFields(x, { b: 10, d: 20 });
console.log(result);
// { a: 1, b: 10, c: 3, d: 20 }
- 泛型接口允许你在定义接口时同时定义一个或多个类型参数
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(100)); // 100
interface GenericDictionary<T> {
[key: string]: T;
}
let stringDict: GenericDictionary<string> = {
first: "Hello",
second: "World",
};
console.log(stringDict["first"]); // Hello
interface Pair<T, U> {
first: T;
second: U;
}
let pair: Pair<number, string> = {
first: 1,
second: "one",
};
console.log(pair.first); // 1
console.log(pair.second); // one
- 泛型类与泛型接口类似
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;
};
interface Lengthwise {
length: number;
}
class Box<T extends Lengthwise> {
content: T;
constructor(content: T) {
this.content = content;
}
printLength() {
console.log(this.content.length);
}
}
let box1 = new Box("Hello, World!");
box1.printLength(); // 13
let box2 = new Box([1, 2, 3, 4, 5]);
box2.printLength(); // 5
- 泛型参数的默认值
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
// 在这个例子中,T 的默认类型是 number
class MyArray<T = number> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
let numberArray = new MyArray(); // 使用默认的 number 类型
numberArray.add(1);
numberArray.add(2);
console.log(numberArray.getItems()); // [1, 2]
let stringArray = new MyArray<string>(); // 明确指定为 string 类型
stringArray.add("Hello");
stringArray.add("World");
console.log(stringArray.getItems()); // ["Hello", "World"]
// 使用另一个类型
let booleanArray = new MyArray<boolean>();
booleanArray.add(true);
booleanArray.add(false);
console.log(booleanArray.getItems()); // [true, false]
- 声明合并
- 如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型
- 函数:可以使用重载定义多个函数类型
- 接口:接口中的属性在合并时会简单的合并到一个接口中
- 同名字段合并时,需要注意类型兼容性
// 约束函数被调用的方式 1
function combine(input1: number, input2: number): number;
// 约束函数被调用的方式 2
function combine(input1: string, input2: string): string;
function combine(input1: number | string, input2: number | string) {
if (typeof input1 === "number" && typeof input2 === "number") {
return input1 + input2;
} else if (typeof input1 === "string" && typeof input2 === "string") {
return input1.concat(input2);
}
}
// ok
combine(1, 2) // 3
// ok
combine('1', '2') // 12
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}
// 等效
interface Alarm {
price: number;
weight: number;
}
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts
- ts