进阶 · TypeScript 入门教程
- 知识点概述
- 字符串字面量类型
- 元组
- 枚举
- 类
- 类与接口
- 泛型
- 声明合并
- 类型别名
type ... = ...
- 使用
type
在创建类型别名 - 类型别名用来给一个类型起个新名字
- 类型别名常与联合类型配合使用
type StringOrNumber = string | number;
let myVar: StringOrNumber;
// ok
myVar = 'Hello';
// ok
myVar = 123;
// error
myVar = 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
// ok
a1 = 1
// ok
a1 = 2
// error
a1 = 3
type t2 = '1' | '2'
let a2: t2
// ok
a2 = '1'
// ok
a2 = '2'
// error
a2 = 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'); // Tom
console.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: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack
class Animal {
static isAnimal(a) {
return a instanceof Animal;
}
}
let a = new Animal('Jack');
Animal.isAnimal(a); // true
a.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); // Jack
a.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";
// private
class Animal {
private name;
public constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
// error 属性“name”为私有属性,只能在类“Animal”中访问。
console.log(this.name);
}
}
// protected
class Animal {
protected name;
public constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
// ok
console.log(this.name);
}
}
- 当构造函数修饰为
**private**
时,该类不允许被继承或者实例化 - 当构造函数修饰为
**protected**
时,该类只允许被继承,但是不允许被实例化
// private
class 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");
// protected
class Animal {
public name;
protected constructor(name) {
this.name = name;
}
}
// ok
class 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.14159
console.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) // 5
console.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);
}
// ok
printDogName(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