【typeScript】深入理解typeScript

深入理解typeScript

相关文章

typeScript易错点浅析


一、Interfaces 和 Type 的区别

语法

  • interface 使用 interface 关键字来定义。
  • type 使用 type 关键字来定义。
1
2
3
interface Person {}

type Person = {};

对象结构

  • interface 仅用于描述对象结构。
  • type 不仅可以描述对象结构,还可以为任何类型创建别名。
1
2
3
4
5
6
7
8
9
10
11
interface Point {
x: number;
y: number;
}

type Point = {
x: number;
y: number;
};

type x : number

可扩展性

  • interface 可以通过 extends 关键字来扩展其他接口。

  • type 不支持扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

// 接口扩展
// Square 接口扩展了 Shape 接口,包含了 color 和 sideLength 属性

type Shape = {
color: string;
};

// 不支持类型别名的扩展
// 由于类型别名不支持扩展,因此无法像接口一样扩展已有类型

合并

  • 如果有多个相同名称的接口声明,则它们会自动合并为单个接口。

  • 对于类型别名,如果有多个相同名称的声明,则会产生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface A {
prop: number;
}

interface A {
otherProp: string;
}

// 合并为单个接口
// 等效于 interface A { prop: number; otherProp: string; }

type B = {
prop: number;
};

type B = {
otherProp: string;
};
// 错误: 重复标识符 'B'

二、JavaScript和TypeScript中 class 的区别

类型检查

TypeScript允许在class的成员(属性和方法)上定义类型注解,以确保在编译时进行类型检查。这有助于在开发过程中捕获潜在的类型错误,并提供更好的代码提示和文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
// 在属性上定义类型注解
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

// 在方法的参数和返回值上定义类型注解
greet(otherPerson: Person): string {
return `Hello, ${otherPerson.name}! My name is ${this.name}.`;
}
}

// 创建一个Person对象
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

// 错误示例:参数类型不匹配
// const greeting: string = person1.greet("Charlie"); // 类型“string”的参数不能赋给类型“Person”的参数

// 正确示例:参数类型匹配
const greeting: string = person1.greet(person2);
console.log(greeting); // 输出:Hello, Bob! My name is Alice.

访问修饰符:

TypeScript引入了访问修饰符(public、private、protected),用于限制class成员的访问权限。这有助于封装和确保代码的安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Person {
// 默认为public,可以在类内外访问
public name: string;
// private只能在类内部访问
private age: number;
// protected只能在类内部和子类中访问
protected gender: string;

constructor(name: string, age: number, gender: string) {
this.name = name;
this.age = age;
this.gender = gender;
}

public introduce(): void {
console.log(`Hello, my name is ${this.name}. I am ${this.age} years old.`);
}
}

class Student extends Person {
constructor(name: string, age: number, gender: string) {
super(name, age, gender);
// 在子类中可以访问protected成员
console.log(`My gender is ${this.gender}.`);
// 错误示例:无法访问父类的private成员
// console.log(`My age is ${this.age}.`); // 属性“age”为私有属性,只能在类“Person”中访问。
}
}

// 创建Person对象
const person = new Person("Alice", 30, "female");

// 正确示例:可以访问public成员
console.log(person.name); // 输出:Alice

// 错误示例:无法访问private成员
// console.log(person.age); // 属性“age”为私有属性,只能在类“Person”中访问。

// 错误示例:无法访问protected成员
// console.log(person.gender); // 属性“gender”受保护,只能在类“Person”及其子类中访问。

// 创建Student对象
const student = new Student("Bob", 25, "male");
student.introduce(); // 输出:Hello, my name is Bob. I am 25 years old.

构造函数参数属性

TypeScript允许在class中的构造函数参数上直接定义属性,从而避免了在构造函数中显式声明属性并进行赋值的步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
// 在构造函数参数上直接定义属性
constructor(public name: string, public age: number) {
// 不需要在构造函数内部显式声明属性和进行赋值
}

public introduce(): void {
console.log(`Hello, my name is ${this.name}. I am ${this.age} years old.`);
}
}

// 创建Person对象
const person = new Person("Alice", 30);

// 正确示例:可以访问构造函数参数属性
console.log(person.name); // 输出:Alice
console.log(person.age); // 输出:30

person.introduce(); // 输出:Hello, my name is Alice. I am 30 years old.

而在js的版本中,这样写属性是不会自动赋值的

接口实现和继承

TypeScript支持接口,可以使用implements关键字来实现接口,以确保class实现了接口中定义的所有成员。此外,TypeScript还支持class之间的继承,可以使用extends关键字来创建class的继承关系。

使用implements关键字来实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个接口
interface Shape {
calculateArea(): number;
}

// 实现接口的类
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}

// 实现接口中定义的方法
calculateArea(): number {
return this.width * this.height;
}
}

假如没有实现了接口中定义的所有成员

class之间的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 定义一个接口
interface Shape {
calculateArea(): number;
}

// 实现接口的类
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}

// 实现接口中定义的方法
calculateArea(): number {
return this.width * this.height;
}
}

// 继承
class Square extends Rectangle {
constructor(sideLength: number) {
super(sideLength, sideLength);
}
}

// 创建Square对象并调用方法
const square = new Square(4);
console.log("Square area:", square.calculateArea()); // 输出:Square area: 16

抽象类

TypeScript支持抽象类,这是一种不能被直接实例化的类,通常用作其他类的基类。抽象类可以包含抽象方法,这些方法必须在子类中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义一个抽象类
abstract class Shape {
// 抽象方法,子类必须实现
abstract calculateArea(): number;
}

// 继承自抽象类的子类
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}

// 实现抽象类中定义的抽象方法
calculateArea(): number {
return this.width * this.height;
}
}

// 错误示例:不能直接实例化抽象类
// const shape = new Shape(); // 错误:无法创建抽象类的实例

// 创建Rectangle对象并调用方法
const rectangle = new Rectangle(5, 10);
console.log("Rectangle area:", rectangle.calculateArea()); // 输出:Rectangle area: 50

类型推断和泛型

TypeScript提供更强大的类型推断能力,并支持泛型,这使得在class中使用更复杂的数据类型变得更容易和安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 泛型类示例
class Box<T> {
private item: T;

constructor(item: T) {
this.item = item;
}

getItem(): T {
return this.item;
}
}

// 创建一个装载数字的盒子
const numberBox = new Box<number>(10);
console.log("Number from box:", numberBox.getItem()); // 输出:Number from box: 10

// 创建一个装载字符串的盒子
const stringBox = new Box<string>("Hello");
console.log("String from box:", stringBox.getItem()); // 输出:String from box: Hello

// 类型推断示例
const inferredBox = new Box("World"); // TypeScript能够推断出传入的参数类型是字符串
console.log("Inferred string from box:", inferredBox.getItem()); // 输出:Inferred string from box: World

三、类型断言

类型断言(Type Assertion)是在TypeScript中用于告诉编译器一个值的具体类型的一种技术。它类似于其它编程语言中的类型转换,但是在编译时不会改变实际的数据结构,只是告诉编译器在编译时将某个值视为特定的类型。TypeScript中有两种方式进行类型断言:

两种方式进行类型断言

尖括号语法

1
2
let someValue: any = "hello world";
let strLength: number = (<string>someValue).length;

as语法

1
2
let someValue: any = "hello world";
let strLength: number = (someValue as string).length;

这两种语法的效果是完全相同的,只是两种风格之间的选择取决于你的偏好或项目的约定。

类型断言可以用于以下几种情况

类型转换

当你从一个更通用的类型转换为一个更具体的类型时,比如从any转换为一个具体的类型。

1
2
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

解决联合类型的类型推断问题

当你处理联合类型时,你可能知道某个值实际上是其中的某个特定类型。

1
2
let someValue: string | number = "hello world";
let strLength: number = (someValue as string).length;

断言为函数类型

你可以使用类型断言指定函数的参数类型和返回类型,这在处理函数式编程和回调函数时很有用。

1
2
3
4
5
interface MyFunction {
(x: number, y: number): number;
}

let myFunction: MyFunction = ((x, y) => x + y) as MyFunction;

断言为类类型

你可以使用类型断言将一个对象视为特定类的实例,从而调用特定类的方法或访问特定类的属性。

1
2
3
4
5
6
7
8
9
10
class Animal {}

class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}

let myAnimal: Animal = new Dog();
(myAnimal as Dog).bark();

四、索引类型

索引类型是TypeScript中一种特殊的类型,它允许我们根据对象的键来访问相应的属性值。在JavaScript中,我们可以使用对象的键来访问属性值,而索引类型在TypeScript中使这种行为更加类型安全。

在TypeScript中,有两种主要的索引类型:字符串索引类型和数字索引类型。

字符串索引类型

字符串索引类型允许我们使用字符串来索引对象的属性。这对于对象的键是字符串的情况非常有用。

1
2
3
4
5
6
7
8
interface Dictionary {
[key: string]: string;
}

let myDictionary: Dictionary = { "a": "apple", "b": "banana" };

console.log(myDictionary["a"]); // 输出:apple
console.log(myDictionary["b"]); // 输出:banana

数字索引类型

数字索引类型允许我们使用数字来索引对象的属性。这对于对象的键是数字的情况非常有用。

1
2
3
4
5
6
7
8
interface NumberArray {
[index: number]: number;
}

let myArray: NumberArray = [1, 2, 3, 4, 5];

console.log(myArray[0]); // 输出:1
console.log(myArray[1]); // 输出:2

五、泛型

泛型(Generics)是一种在编程语言中用于创建可重用、通用代码的技术。它允许在定义函数、类或接口时使用类型参数,这些类型参数可以在使用时被具体指定,从而增加代码的灵活性和复用性。泛型使得我们能够编写与数据类型无关的通用代码,从而提高代码的可维护性和可扩展性。

基本语法

泛型的基本语法包括使用尖括号(< >)来指定类型参数,将类型参数放在函数名或类名后面,并在函数体或类体中使用这些类型参数。

函数泛型

1
2
3
4
5
6
7
function identity<T>(arg: T): T {
return arg;
}

let output = identity<string>("hello");
// 或者可以通过类型推断来省略类型参数
let output = identity("hello");

在上面的示例中,<T>表示identity函数接受一个类型参数T,并返回类型为T的参数。当调用identity函数时,可以显式指定类型参数(例如<string>),也可以通过类型推断省略类型参数。

类泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Box<T> {
private item: T;

constructor(item: T) {
this.item = item;
}

getItem(): T {
return this.item;
}
}

let numberBox = new Box<number>(10);
let stringBox = new Box<string>("hello");

在上面的示例中,Box<T>表示Box类接受一个类型参数T,在创建Box类的实例时,可以通过尖括号指定具体的类型参数。

泛型约束

有时候我们需要对泛型进行约束,以确保某些操作在所有类型上都是有效的。泛型约束可以通过扩展(extends)关键字来实现。

1
2
3
4
5
6
7
8
9
10
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 在泛型约束中使用length属性
return arg;
}

loggingIdentity({ length: 10, value: 3 }); // 输出:10

在上面的示例中,T extends Lengthwise表示泛型T必须符合Lengthwise接口的约束,即具有length属性。这样,我们就可以在泛型函数loggingIdentity中使用arg.length,而不会出现类型错误。

复杂点的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Stack<T> {
private elements: T[] = [];

push(element: T): void {
this.elements.push(element);
}

pop(): T | undefined {
return this.elements.pop();
}

peek(): T | undefined {
return this.elements[this.elements.length - 1];
}

isEmpty(): boolean {
return this.elements.length === 0;
}
}

// 使用泛型的堆栈
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);

console.log("Peek:", numberStack.peek()); // 输出:Peek: 3

while (!numberStack.isEmpty()) {
console.log("Pop:", numberStack.pop()); // 输出:Pop: 3 Pop: 2 Pop: 1
}

在上面的示例中,我们定义了两个接口 Dog 和 Cat,分别表示狗和猫。然后我们使用交叉类型 Dog & Cat 将这两个接口合并为一个新的类型 DogCat。最后,我们创建了一个 DogCat 类型的变量 pet,它具有 Dog 和 Cat 接口中的所有成员,因此可以调用 bark() 和 meow() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function mapTuple<T, U>(tuple: [T, T], fn: (item: T) => U): [U, U] {
return [fn(tuple[0]), fn(tuple[1])];
}

// 定义一个转换函数,将字符串转换为大写
function toUpperCase(str: string): string {
return str.toUpperCase();
}

// 调用 mapTuple 函数,并传入一个字符串元组和转换函数
const originalTuple: [string, string] = ["hello", "world"];
const uppercasedTuple: [string, string] = mapTuple(originalTuple, toUpperCase);

console.log(uppercasedTuple); // 输出:[ 'HELLO', 'WORLD' ]

在这个例子中,mapTuple 函数有两个泛型参数 T 和 U。T 表示输入元组的元素类型,U 表示转换后的元素类型。该函数接受一个元组 tuple 和一个转换函数 fn 作为参数,然后将转换函数应用于元组的每个元素,并返回一个新的元组。

六、交叉类型

交叉类型(Intersection Types)是 TypeScript 中一种用于组合多种类型的机制。通过交叉类型,我们可以将多个类型合并为一个类型,从而创建出一个新的类型,它具有所有类型的成员。

使用 & 符号来表示交叉类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Dog {
bark(): void;
}

interface Cat {
meow(): void;
}

// 定义一个交叉类型,该类型具有 Dog 和 Cat 接口中的所有成员
type DogCat = Dog & Cat;

// 创建一个 DogCat 类型的变量
let pet: DogCat = {
bark() {
console.log('Woof! Woof!');
},
meow() {
console.log('Meow! Meow!');
}
};

pet.bark(); // 输出:Woof! Woof!
pet.meow(); // 输出:Meow! Meow!

七、any类型

any 类型是 TypeScript 中的一种特殊类型,它表示任意类型。当我们将一个变量声明为 any 类型时,TypeScript 将不会对这个变量进行类型检查,从而允许我们在编码时可以使用任何类型的值赋给这个变量,也可以调用任何方法,访问任何属性,甚至可以对它进行任何操作,而不会报错。

any 类型的主要特点包括:

类型推断

如果不显示指定类型,TypeScript 会将不带类型注释的变量自动推断为 any 类型。

1
2
3
let variable; // 推断为 any 类型
variable = 10; // 合法
variable = "hello"; // 合法

兼容性

any 类型兼容所有类型,所有类型也兼容 any 类型。

1
2
let value: any = 10;
let numberValue: number = value; // 合法,但可能在运行时出错

取消类型检查

在使用 any 类型时,TypeScript 不会对它进行类型检查,因此可以绕过类型检查,但也会失去 TypeScript 的类型安全性。

1
2
let data: any;
data.someMethod(); // 合法,但可能在运行时出错

灵活性

any 类型可以让我们处理动态类型的数据,或者处理无法确定类型的情况,例如从第三方库中获取的数据。

1
2
let dynamicData: any = fetchDataFromThirdParty();
// 在处理从第三方库获取的数据时,可以将其声明为 any 类型以适应不同的数据类型

八、keyof关键字

keyof 是 TypeScript 中的一个关键字,用于获取某个类型的所有键(属性名)的联合类型。它常用于与泛型结合,以提取类型的键集合并进行类型约束或操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Person {
username: string;
age: number;
email: string;
}

type PersonKey = keyof Person;
// PersonKey 的类型为 "username" | "age" | "email"

function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

const person: Person = {
username: "John",
age: 30,
email: "john@example.com"
};

const username = getProperty(person, "username"); // 类型推断为 string
const age = getProperty(person, "age"); // 类型推断为 number
const email = getProperty(person, "email"); // 类型推断为 string

console.log(username)
console.log(age)
console.log(email)

九、命名空间

命名空间(Namespace)是 TypeScript 中用来组织代码的一种方式。它类似于其他编程语言中的模块(Module)的概念,但在 TypeScript 中,命名空间主要用于在全局范围内组织代码,并防止命名冲突。

基本语法

在 TypeScript 中,使用 namespace 关键字来定义命名空间,然后在命名空间中定义变量、函数、类等。命名空间内的成员只在该命名空间内部可见,如果要在外部访问命名空间内的成员,需要使用命名空间的全限定名称。

1
2
3
4
5
6
7
8
9
10
11
namespace MyNamespace {
export const foo = 123;

export function bar() {
console.log("bar");
}

export class Baz {
constructor(public name: string) {}
}
}

使用方法

导出成员

命名空间中的成员默认是私有的,如果要在外部访问,需要使用 export 关键字进行导出。

1
2
3
namespace MyNamespace {
export const foo = 123;
}

访问命名空间成员

可以使用点运算符来访问命名空间中的成员,也可以使用全限定名称。

1
2
3
4
MyNamespace.foo; // 123
MyNamespace.bar(); // "bar"
const baz = new MyNamespace.Baz("example");
console.log(baz.name); // "example"

嵌套命名空间

命名空间也可以嵌套定义,以进一步组织代码。

1
2
3
4
5
namespace OuterNamespace {
export namespace InnerNamespace {
export const x = 10;
}
}

十、never类型

在 TypeScript 中,never 类型表示的是那些永远不会出现的值的类型。它通常用于以下几种情况:

函数返回值

当一个函数永远不会正常返回时,可以将其返回类型标注为 never。

1
2
3
function throwError(message: string): never {
throw new Error(message);
}

在这个例子中,throwError 函数抛出一个错误,因此它永远不会正常返回,因此返回类型是 never。

永远不会发生的条件

在某些条件判断中,编译器可以推断出某些分支永远不会执行,因此可以将其类型标注为 never。

1
2
3
4
5
6
7
8
9
10
11
12
13
function checkType(x: string | number) {
if (typeof x === "string") {
// 在这个分支中,x 的类型被推断为 string
console.log(x.toUpperCase());
} else if (typeof x === "number") {
// 在这个分支中,x 的类型被推断为 number
console.log(x.toFixed(2));
} else {
// 在这个分支中,x 被推断为 never 类型,因为它是不可能发生的条件
const exhaustiveCheck: never = x;
console.log(exhaustiveCheck); // 此行会报错
}
}

在这个例子中,由于 x 可能是 string 或 number,因此在前两个条件分支中,编译器能够推断出 x 的具体类型。但是,在最后一个条件分支中,由于前面的条件已经覆盖了所有可能的类型,因此这个条件分支是永远不会执行的,所以 x 被推断为 never 类型。

空数组

never[] 类型表示一个永远不会包含任何元素的数组。

1
let emptyArray: never[] = [];

never 类型在 TypeScript 中通常用于表示不可能发生的值或情况,它可以帮助开发者更准确地描述代码的行为,并帮助编译器进行类型检查。


喜欢这篇文章?打赏一下支持一下作者吧!
【typeScript】深入理解typeScript
https://www.cccccl.com/20230926/typeScript/typeScript易错点浅析/
作者
Jeffrey
发布于
2023年9月26日
许可协议