深入浅出TypeScript
TypeScript是一种由微软开发的开源、跨平台的编程语言。它是JavaScript的超集,最终会被编译为JavaScript代码。
为什么要学习TypeScript
TypeScript 是 JavaScript 的超集,它添加了静态类型和一些其他特性。
TypeScript 在语法上与 JavaScript 兼容,可以使用现有的 JavaScript 代码和库,但还提供了额外的类型系统、面向对象编程的特性、模块化支持等。TypeScript 在编译时会将 TypeScript 代码转换为 JavaScript 代码,然后可以在浏览器或服务器中运行。
以下是 TypeScript 相对于 JavaScript 的一些优势:
- 静态类型检查:TypeScript 引入了静态类型系统,可以在编译时检测类型错误,提供更早的错误捕获和更好的代码提示。这有助于减少运行时错误,并提高代码的可靠性和可维护性。
- 强类型,支持静态和动态类型。
- 更好的工具支持:TypeScript 提供了丰富的开发工具支持,包括代码编辑器的智能提示、自动完成、重构等功能。这些工具可以提高开发效率,并减少常见错误。
- 更强大的面向对象编程支持:TypeScript 支持类、接口、泛型等面向对象编程的特性。这使得代码可以更结构化、可复用,并且可以使用面向对象的设计模式。
- 更好的模块化支持:TypeScript 提供了模块化的语法和命名空间的概念,可以更好地组织和管理代码,避免全局命名冲突。
- 渐进式采用:TypeScript 可以与现有的 JavaScript 代码和库无缝集成。可以将现有的 JavaScript 项目逐步迁移到 TypeScript,从而享受到 TypeScript 的优势,而无需重写整个代码库。
(TypeScript不允许改变变量的数据类型)
TypeScript推荐
Awesome Typescript: TS开源教程及应用
Typescript Playground: TS到JS在线编译
TypeScript基础
基础类型
TypeScript 提供了一些基础类型来表示常见的数据类型。以下是 TypeScript 的一些基础类型:
- Boolean(布尔类型) :表示逻辑值,可以是
true
或false
。 - Number(数字类型) :表示数值,可以是整数或浮点数。
- String(字符串类型) :表示文本字符串,可以使用单引号或双引号来表示字符串。
- Array(数组类型) :表示一个元素的集合,可以包含多个值,并且这些值可以是相同类型或不同类型。
- Tuple(元组类型) :表示一个固定长度的数组,每个元素的类型可以是不同的。
- Enum(枚举类型) :表示一组具名的常量值,可以通过枚举成员的名称来引用。
- Any(任意类型) :表示任意类型的值,可以绕过类型检查。
- Unknown(未知类型) :表示未知的值,需要进行类型检查或类型断言来确定具体类型。
- Void(空类型) :表示没有任何类型,通常用于函数没有返回值的情况。
- Never(永不存在的类型) :表示永远不会出现的类型,通常用于表示抛出异常或无法正常结束的函数。
- Null 和 Undefined(空值类型) :表示变量可以存储
null
或undefined
。 - Object(对象类型) :表示非原始类型的值,如函数、数组、类等。
这些基础类型可以用于声明变量、函数参数、函数返回值等。此外,TypeScript 还支持类型推导,可以根据上下文自动推断变量的类型,避免显式指定类型。
例如,下面是使用基础类型的一些示例:
ini
let isDone: boolean = false;
let age: number = 25;
let name: string = "John";
let numbers: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 42];
enum Color { Red, Green, Blue };
let color: Color = Color.Red;
let anyValue: any = "any value";
let unknownValue: unknown;
let voidValue: void = undefined;
let nullValue: null = null;
let undefinedValue: undefined = undefined;
let neverValue: never = (() => { throw new Error("Error"); })();
let obj: object = { key: "value" };
// 类型检查和类型断言
if (typeof unknownValue === "number") {
let numberValue: number = unknownValue; // 正确,已经确定 unknownValue 是数字类型
}
let stringValue: string = unknownValue; // 错误,不能将 unknown 类型赋值给 string 类型
let anyValue2: any = unknownValue; // 正确,unknown 类型可以赋值给 any 类型
函数类型
在 TypeScript 中,函数也可以具有类型。函数类型可以描述函数的参数类型、返回值类型以及函数本身的类型。
-
函数声明
使用函数声明语法来定义函数类型,包括参数类型和返回值类型。
typescriptfunction add(x: number, y: number): number { return x + y; }
-
函数表达式
使用函数表达式语法来定义函数类型,包括参数类型和返回值类型。
typescriptconst multiply = function (x: number, y: number): number { return x * y; };
-
箭头函数表达式
使用箭头函数表达式语法来定义函数类型,包括参数类型和返回值类型。
typescriptconst subtract = (x: number, y: number): number => x - y;
-
函数类型注解
可以使用类型注解来明确函数的类型。
typescriptlet divide: (x: number, y: number) => number; divide = (x, y) => x / y;
-
可选参数和默认参数
函数类型中可以使用可选参数和默认参数。
typescriptfunction greet(name: string, greeting?: string): void { if (greeting) { console.log(`${greeting}, ${name}!`); } else { console.log(`Hello, ${name}!`); } } greet("John"); // Hello, John! greet("Jane", "Hi"); // Hi, Jane!
-
剩余参数
函数类型中可以使用剩余参数语法来接收不定数量的参数。
typescriptfunction sum(...numbers: number[]): number { return numbers.reduce((acc, curr) => acc + curr, 0); } console.log(sum(1, 2, 3)); // 6 console.log(sum(4, 5, 6, 7, 8)); // 30
-
重载
可以使用函数重载来定义多个函数签名,以适应不同的参数类型和返回值类型。
typescriptfunction processInput(input: string): string; function processInput(input: number): number; function processInput(input: string | number): string | number { if (typeof input === "string") { return input.toUpperCase(); } else { return input * 2; } } console.log(processInput("hello")); // "HELLO" console.log(processInput(5)); // 10
接口
在 TypeScript 中,接口(Interfaces)用于定义对象的结构和类型。接口提供了一种约束和规范对象的方式,使得我们可以在代码中明确指定对象应该具有的属性和方法。
特点:
- 可选属性: ?
- 只读属性: read-only
- 可以描述函数类型
- 可以描述自定义属性
下面是 TypeScript 中接口的基本语法:
css
interface MyInterface {
property1: type;
property2: type;
method1(): returnType;
method2(param: type): returnType;
}
通过上述语法,我们可以定义一个名为 MyInterface
的接口,该接口具有两个属性 property1
和 property2
,以及两个方法 method1
和 method2
。每个属性和方法都可以指定其类型。
接口还可以包含可选属性和只读属性:
typescript
interface MyInterface {
readonly property1: type;
property2?: type;
}
在上述示例中,property1
是一个只读属性,它的值在对象创建后不能被修改,而 property2
是一个可选属性,可以存在也可以不存在。
接口还可以用于描述函数类型:
go
interface MyFunction {
(param1: type, param2: type): returnType;
}
上述示例中的接口 MyFunction
描述了一个函数类型,该函数接受两个参数,并返回一个特定类型的值。
接口还支持继承,一个接口可以继承自其他接口:
typescript
interface ParentInterface {
property1: type;
}
interface ChildInterface extends ParentInterface {
property2: type;
}
在上述示例中,ChildInterface
继承了 ParentInterface
,因此它具有 property1
和 property2
两个属性。
除了对象和函数类型,接口还可以用于类的实现:
typescript
interface MyInterface {
method1(): void;
}
class MyClass implements MyInterface {
method1() {
// 实现接口中定义的方法
}
}
通过 implements
关键字,我们可以让一个类实现一个或多个接口,并确保类中实现了接口中定义的所有属性和方法。
类
定义:写法和JS差不多,增加了一些定义
特点:
-
增加了 public、private、protected 修饰符
-
抽象类:
- 只能被继承,不能被实例化
- 作为基类,抽象方法必须被子类实现
-
interface约束类,使用implements关键字
修饰符(Modifiers):
TypeScript 中的类可以使用修饰符来限制属性和方法的访问性。常用的修饰符有:
public
:默认的修饰符,表示属性或方法可以在类的内部和外部访问。private
:表示属性或方法只能在类的内部访问。protected
:表示属性或方法可以在类的内部和子类中访问。
typescript
class MyClass {
public publicProperty: type;
private privateProperty: type;
protected protectedProperty: type;
public publicMethod() {
// 可以在类内部和外部访问
}
private privateMethod() {
// 只能在类内部访问
}
protected protectedMethod() {
// 可以在类内部和派生类中访问
}
}
抽象类(Abstract Classes):
抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类可以包含抽象方法(abstract method
),这些方法在抽象类中只有声明,而没有具体的实现。子类必须实现抽象类中的抽象方法,否则子类也必须声明为抽象类。
抽象类常用于作为基类,提供一些通用的属性和方法的定义,而具体的实现则由子类来完成。抽象类可以帮助我们实现代码的重用和继承的规范性。
typescript
abstract class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
abstract makeSound(): void;
move(distance: number): void {
console.log(`${this.name} moved ${distance} meters.`);
}
}
class Dog extends Animal {
makeSound(): void {
console.log(`${this.name} barks.`);
}
}
class Cat extends Animal {
makeSound(): void {
console.log(`${this.name} meows.`);
}
}
const dog = new Dog('Buddy');
dog.makeSound(); // Output: Buddy barks.
dog.move(10); // Output: Buddy moved 10 meters.
const cat = new Cat('Whiskers');
cat.makeSound(); // Output: Whiskers meows.
cat.move(5); // Output: Whiskers moved 5 meters.
在上面的示例中,Animal
类是一个抽象类,它有一个受保护的属性 name
和两个方法:makeSound
和 move
。makeSound
方法是一个抽象方法,没有具体的实现,因此在子类中必须实现该方法。
Dog
和 Cat
类分别继承了 Animal
类,并实现了 makeSound
方法。它们可以通过实例化来创建具体的动物对象,并调用继承自 Animal
类的方法。
需要注意的是,抽象类不能被直接实例化,只能作为基类供其他类继承。抽象类提供了一种抽象的模板,定义了共享的属性和方法,而具体的实现则由子类来完成。这样可以确保子类按照约定进行实现,并提供了一种规范和契约的方式。
接口约束类(Interface with Classes):
TypeScript 中的接口不仅可以用于描述对象的结构,还可以用于约束类的实现。通过使用 implements
关键字,我们可以让一个类实现一个或多个接口。实现接口的类必须实现接口中定义的所有属性和方法。
typescript
interface Shape {
calculateArea(): number;
display(): void;
}
class Circle implements Shape {
private radius: number;
constructor(radius: number) {
this.radius = radius;
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
display(): void {
console.log(`Circle with radius ${this.radius}`);
}
}
class Rectangle implements Shape {
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
calculateArea(): number {
return this.width * this.height;
}
display(): void {
console.log(`Rectangle with width ${this.width} and height ${this.height}`);
}
}
const circle = new Circle(5);
console.log(circle.calculateArea()); // Output: 78.53981633974483
circle.display(); // Output: Circle with radius 5
const rectangle = new Rectangle(3, 4);
console.log(rectangle.calculateArea()); // Output: 12
rectangle.display(); // Output: Rectangle with width 3 and height 4
在上面的示例中,我们定义了一个 Shape
接口,它约定了两个方法:calculateArea
和 display
。这些方法描述了一个形状对象应该具备的行为。
Circle
类和 Rectangle
类都实现了 Shape
接口,它们分别提供了计算面积和显示形状的具体实现。
通过接口的约束,我们可以确保任何实现 Shape
接口的类都必须实现 calculateArea
和 display
方法。这样,我们可以在代码中使用 Shape
类型的变量或参数,而无需关心具体的实现类是什么,只需要确保它们满足 Shape
接口的约定。
TypeScript进阶
高级类型
联合类型
当我们需要表示一个变量或参数可以是多个类型之一时,可以使用 TypeScript 中的联合类型。联合类型使用 |
符号将多个类型组合在一起,表示该值可以是其中任意一个类型。下面是一个使用联合类型的示例:
typescript
function printId(id: number | string): void {
console.log(`ID: ${id}`);
}
printId(123); // Output: ID: 123
printId("abc"); // Output: ID: abc
在上面的示例中,printId
函数接受一个参数 id
,它的类型被定义为 number | string
,表示 id
可以是一个数字或一个字符串。在函数内部,我们可以根据 id
的具体类型来执行不同的逻辑。
联合类型的好处是它提供了灵活性,允许我们处理多种类型的值。然而,需要注意的是,在使用联合类型时,我们只能访问所有类型共有的属性和方法,因为 TypeScript 并不知道具体是哪个类型。
交叉类型
交叉类型使用 &
符号将多个类型合并成一个类型,表示该类型同时具备多个类型的特性。下面是一个使用交叉类型的示例:
css
interface A {
propA: number;
}
interface B {
propB: string;
}
function combineObjects(obj1: A, obj2: B): A & B {
return { ...obj1, ...obj2 };
}
const objA: A = { propA: 123 };
const objB: B = { propB: "abc" };
const combinedObj: A & B = combineObjects(objA, objB);
console.log(combinedObj); // Output: { propA: 123, propB: "abc" }
在上面的示例中,我们定义了两个接口 A
和 B
,它们分别具有不同的属性。然后,我们编写了一个函数 combineObjects
,它接受两个参数 obj1
和 obj2
,分别是类型 A
和类型 B
的对象。函数的返回类型被定义为 A & B
,表示返回的对象同时具备类型 A
和类型 B
的属性。
通过交叉类型,我们可以将多个类型的特性合并到一个类型中,从而创建更复杂的类型。这在需要同时满足多个类型特性的场景中非常有用。
类型断言
类型断言是一种在编译时告诉 TypeScript 某个值的具体类型的方式。它使用 as
关键字或尖括号语法进行表示。下面是一个使用类型断言的示例:
typescript
let value: any = "hello";
let length: number = (value as string).length;
console.log(length); // Output: 5
在上面的示例中,我们将一个值 value
声明为 any
类型,这意味着它可以是任意类型。然后,我们使用类型断言 (value as string)
告诉 TypeScript 将 value
视为字符串类型。接着,我们可以访问字符串类型的属性 length
。
类型断言的作用是在某些情况下,我们比 TypeScript 更了解变量的具体类型,可以使用类型断言来告诉 TypeScript 该变量的类型,从而获得更好的类型检查和代码提示。
类型别名 (type VS interface)
类型别名和接口都可以用来定义自定义类型,但它们有一些不同之处。
-
定义:给类型起个别名
-
相同点:
都可以定义对象或函数
都允许继承
-
差异点:
interface是TS用来定义对象,type是用来定义别名方便使用
type可以定义基本类型,interface不行
interface可以合并重复声明,type不行
类型别名使用 type
关键字进行定义,可以给现有类型起一个新的名字。类型别名可以表示联合类型、交叉类型、函数类型等复杂的类型。下面是一个使用类型别名的示例:
ini
type Point = {
x: number;
y: number;
};
type Shape = Circle | Rectangle;
type CalculateArea = (shape: Shape) => number;
在上面的示例中,我们使用类型别名定义了 Point
类型,表示一个具有 x
和 y
属性的点。然后,我们使用类型别名 Shape
表示 Circle
类型和 Rectangle
类型的联合类型。最后,我们使用类型别名 CalculateArea
表示一个函数类型,接受 Shape
类型的参数并返回一个数字。
类型别名和接口的选择取决于具体的使用场景。一般来说,如果需要描述对象的形状和行为,应该使用接口;如果需要表示复杂的类型,如联合类型或交叉类型,应该使用类型别名。 此外,接口可以被类实现(implements),而类型别名不能。
泛型
当我们需要编写可重用的代码,并且希望在不同的地方使用相同的逻辑但操作不同类型的数据时,可以使用 TypeScript 中的泛型(Generics)。泛型允许我们在定义函数、类或接口时使用类型参数,这样我们就可以在使用时指定具体的类型。
基本定义:
-
泛型的语法是今里面写类型参数,一般用T表示
-
使用时有两种方法指定类型:
- 定义要使用的类型
- 通过TS类型推断,自动推导类型
-
泛型的作用是临时占位,之后通过传来的类型进行推导(通常为T)
luafunction print<T>(arg:T):T { console.log(arg) return arg } print<string>('hello') // 定义T为 stringprint('hello') print('hello') // TS 类型推断,自动推导类型为 string
下面是一个使用泛型的简单示例:
typescript
function identity<T>(arg: T): T {
return arg;
}
let result = identity<string>("Hello");
console.log(result); // Output: Hello
let anotherResult = identity<number>(123);
console.log(anotherResult); // Output: 123
在上面的示例中,我们定义了一个名为 identity
的函数,它接受一个参数 arg
,类型为 T
。函数的返回类型也是 T
。这里的 T
是一个类型参数,表示我们在使用函数时可以指定具体的类型。
在调用 identity
函数时,我们使用尖括号语法 <string>
和 <number>
分别指定了 T
的具体类型为字符串和数字。这样,函数内部的参数和返回值的类型就会根据指定的类型进行推断和检查。
泛型不仅可以用于函数,还可以用于类和接口。下面是一个使用泛型的类的示例:
typescript
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
let container = new Container<string>("Hello");
console.log(container.getValue()); // Output: Hello
let anotherContainer = new Container<number>(123);
console.log(anotherContainer.getValue()); // Output: 123
在上面的示例中,我们定义了一个名为 Container
的类,它有一个类型参数 T
。类的构造函数接受一个参数 value
,类型为 T
。类中的方法 getValue
返回类型为 T
的值。
在创建 Container
对象时,我们使用尖括号语法分别指定了 T
的具体类型为字符串和数字。这样,创建的对象就具有相应的类型,并且可以调用相应类型的方法。
泛型工具类型-基础操作符
-
typeof:获取类型
iniinterface Person { name: string; age: number; } const sem: Person = { name: "semlinker", age: 30 }; type Sem = typeof sem; // type Sem = Person
-
keyof: 获取所有键
luainterface Person { name: string; age: number; } type K1 = keyof Person; // "name" | "age" type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
-
in:遍历枚举类型
pythontype Keys ="a" | "b" | "c" type Obj ={ [p in Keys]: any } // -> { a: any, b: any, c: any }
-
T[K]: 索引访问
typescriptinterface IPerson { name: string; age: number; } let typel: IPerson['name'] // string let type2: IPerson['age'] // number
-
extends:泛型约束
phpinterface Lengthwise { length: number; } function loggingIdentity<T extends lLengthwise>(arg: T): T (console.log(arg.length);return arg; loggingIdentity({value:3))loggingIdentity((length: 5})
泛型工具类型-常用工具类型
-
Partial
:将类型属性变为可选initype Partial<T> = { [p in keyof T]?: T[P]; }
-
Required
将类型属性变为必选rtype Required<T> = { [P in keyof T]-?: T[P] }
-
Readonly
:将类型属性变为只读initype Readonly<T> = { readonly [p in keyof T]: T[P]; }
-
Exclude<T, U>
:从类型T
中排除可以赋值给类型U
的部分。它会创建一个新的类型,其中排除了T
中与U
可赋值的部分。initype Numbers = 1 | 2 | 3 | 4 | 5; type ExcludeTwoAndThree = Exclude<Numbers, 2 | 3>; // ExcludeTwoAndThree 的类型为 1 | 4 | 5
-
Extract<T, U>
:从类型T
中提取可以赋值给类型U
的部分。它会创建一个新的类型,其中提取了T
中与U
可赋值的部分。initype Numbers = 1 | 2 | 3 | 4 | 5; type ExtractTwoAndThree = Extract<Numbers, 2 | 3>; // ExtractTwoAndThree 的类型为 2 | 3
-
Omit<T, K>
:从类型T
中删除指定的属性K
,创建一个新的类型。它可以用于从一个类型中排除指定的属性。iniinterface User { name: string; age: number; email: string; } type UserWithoutEmail = Omit<User, "email">; // UserWithoutEmail 的类型为 { name: string; age: number; }
-
Record<K, T>
:创建一个新的类型,其中属性名为类型K
中的值,属性值为类型T
。它可以用于创建具有特定键和值类型的对象。 -
ReturnType<T>
:获取函数类型T
的返回类型。它会创建一个新的类型,表示函数T
的返回值类型。示例:csharpfunction greet(): string { return "Hello!"; } type Greeting = ReturnType<typeof greet>; // Greeting 的类型为 string
TypeScript实战
声明文件
TypeScript 的声明文件(Declaration Files)用于描述已有 JavaScript 代码库的类型信息。通过声明文件,我们可以为 JavaScript 库提供类型定义,使得在使用这些库时可以获得类型检查和智能提示的支持。以下是关于 TypeScript 声明文件的一些重要信息:
- 声明文件的后缀名为
.d.ts
。例如,如果要为lodash
库提供类型定义,可以创建一个名为lodash.d.ts
的声明文件。 - 声明文件中使用 TypeScript 的语法来描述类型信息。可以使用接口(interface)、类型别名(type)、枚举(enum)等 TypeScript 的特性来定义类型。
- 声明文件中只需要描述类型信息,不需要包含实际的 JavaScript 代码。它们的目的是为了提供类型检查和智能提示,而不会影响运行时的行为。
- 在声明文件中,可以使用
declare
关键字来声明变量、函数、类等。这样 TypeScript 就知道它们是从外部导入的类型,而不是当前文件中的实现。 - 声明文件可以通过多种方式进行引入,包括直接引入、通过
/// <reference path="..." />
引入、通过import
语句引入等。 - 对于已经发布到 npm 上的 JavaScript 库,通常可以在 DefinitelyTyped 社区维护的
@types
组织中找到对应的声明文件。可以使用 npm 安装对应的@types
包,或者手动下载声明文件并引入到项目中。 - 如果找不到现成的声明文件,也可以手动编写自己的声明文件。可以参考已有的 JavaScript 代码和文档来了解库的 API,并根据需要编写相应的类型定义。
- declare:三方库需要类型声明文件
- dts:声明文件定义
- @types:三方库TS类型包
- tsconfig.json:定义TS的配置
泛型约束后端接口类型
在 TypeScript 中,我们可以使用泛型约束来限制后端接口的类型。通过泛型约束,我们可以确保传入的参数符合特定的类型要求,从而提供更强大的类型检查和智能提示。
typescript
interface ApiResponse<T> {
success: boolean;
data: T;
}
interface User {
id: number;
name: string;
email: string;
}
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
// 在实际的代码中,可能会发起网络请求获取后端数据
// 这里只是模拟一个异步操作,并返回一个 Promise 对象
return new Promise<ApiResponse<T>>((resolve, reject) => {
setTimeout(() => {
// 假设这里是从后端获取到的数据
const responseData: T = { id: 1, name: "John", email: "john@example.com" };
const apiResponse: ApiResponse<T> = {
success: true,
data: responseData,
};
resolve(apiResponse);
}, 1000);
});
}
// 使用泛型约束调用 fetchData 函数
fetchData<User>("/api/user").then((response) => {
if (response.success) {
const user: User = response.data;
console.log(user.id, user.name, user.email);
}
});
在上面的示例中,ApiResponse<T>
是一个泛型接口,它接受一个类型参数 T
,表示后端接口返回的数据类型。fetchData
函数也是一个泛型函数,它接受一个 url 参数,并返回一个 Promise 对象,该 Promise 对象的泛型参数是 ApiResponse<T>
。
在调用 fetchData<User>("/api/user")
时,我们通过泛型约束指定了 T
的具体类型为 User
,这样 TypeScript 就知道返回的 response.data
是一个 User
类型的对象,可以进行相应的类型检查和智能提示。
通过使用泛型约束后端接口类型,我们可以在编译时捕获类型错误,并获得更好的类型推断和类型安全性。