TypeScript - 类型兼容性



在 TypeScript 中,类型兼容性是指将一种类型的变量、对象等赋值给另一种类型的能力。换句话说,它指的是根据类型的结构检查两种类型是否兼容的能力。

例如,字符串和布尔类型彼此不兼容,如下面的代码所示。

let s:string = "Hello";
let b:boolean = true;
s = b; // Error: Type 'boolean' is not assignable to type 'string'
b = s; // Error: Type 'string' is not assignable to type 'boolean'

TypeScript 的类型系统允许执行编译时不安全的某些操作。例如,任何类型的变量都与 'any' 类型兼容,这是不安全的行为。

例如:

let s: any = 123;
s = "Hello"; // Valid

TypeScript 如何执行类型兼容性检查?

Typescript 使用结构化子类型和结构化赋值来执行类型兼容性检查。让我们通过示例学习每一个。

结构化子类型

TypeScript 使用结构化子类型方法来检查特定类型是否为另一种类型的子类型。即使特定类型的成员名称不匹配,但结构匹配,TypeScript 编译器也会将这两种类型视为相同。

例如:

interface Person {
    name: string;
}

let person: Person;
let obj = { name: "John", age: 30 };

// Ok
person = obj;

要检查 'obj' 对象的类型是否与 Person 接口的类型兼容,typescript 会检查 'obj' 是否至少包含 Person 接口中包含的所有属性和方法。

TypeScript 不关心添加到子类型的额外成员。这里,obj 对象包含一个额外的 'age' 属性,但它仍然与 Person 类型兼容,因为 obj 对象包含字符串类型的 'name' 属性。

如果 'obj' 对象不包含 Person 接口的所有成员,则它不能赋值给具有 Person 类型的对象。例如:

interface Person {
    name: string;
}

let person: Person;
let obj = { n: "John", age: 30 };

// Not Ok
person = obj;

当我们编译上述代码时,它会抛出错误,因为 'obj' 对象的类型与 Person 接口不同。

如何有效地使用类型兼容性?

开发人员可以使用接口和泛型来有效地在 TypeScript 中使用类型。以下是使用接口和泛型进行类型兼容性的最佳技巧。

使用接口

使用接口,开发人员可以定义契约或类型,以确保实现符合这些类型。这有助于确保代码不同部分之间的类型兼容性。

让我们通过下面的示例来了解它。

示例

在下面的示例中,'user2' 变量的类型为 'User'。因此,开发人员可以将具有与 'User' 接口相同属性的对象赋值给 'user2' 对象。

interface User {
    name: string;
    age: number;
}

const user = { name: "Alice", age: 30 };
let user2: User = user;
console.log(user2)

编译后,它将生成以下 JavaScript 代码。

const user = { name: "Alice", age: 30 };
let user2 = user;
console.log(user2);

输出

其输出如下:

{ name: 'Alice', age: 30 }

使用泛型

我们可以使用泛型来创建可重用的组件,这些组件可以与多种数据类型一起工作,而不是单一数据类型。它允许开发人员将类型作为参数传递,并将其用于变量、对象、类、函数参数等。

让我们通过下面的示例来了解它。

示例

在下面的代码中,我们有一个 'Wrapper' 接口,它将数据类型作为参数。我们创建了 stringWrapper 和 numberWrapper 变量,并将字符串和数字数据类型作为参数传递。

// Define a generic interface with a single property
interface Wrapper<T> {
    value: T;
}

// Use the interface with a string type
let stringWrapper: Wrapper<string> = {
    value: "Hello, TypeScript!",
};

// Use the interface with a number type
let numberWrapper: Wrapper<number> = {
    value: 123,
};

console.log(stringWrapper.value); // Output: Hello, TypeScript!
console.log(numberWrapper.value); // Output: 123

编译后,它将生成以下 JavaScript 代码。

// Use the interface with a string type
let stringWrapper = {
    value: "Hello, TypeScript!",
};

// Use the interface with a number type
let numberWrapper = {
    value: 123,
};
console.log(stringWrapper.value); // Output: Hello, TypeScript!
console.log(numberWrapper.value); // Output: 123

输出

上述示例代码将产生以下输出:

Hello, TypeScript!
123

函数和类型兼容性

当我们比较或将一个函数赋值给另一个函数时,TypeScript 编译器会检查目标函数是否至少具有与源函数相同的参数和返回类型。如果您将函数 'x' 赋值给函数 'y',则函数 'x' 是目标函数,函数 'y' 是源函数。函数 'y' 中的附加参数不会导致任何错误。

示例

在下面的代码中,函数 'x' 只包含 1 个参数,而函数 'y' 包含 2 个参数。当我们将函数 'x' 赋值给函数 'y' 时,附加参数 's' 不会导致任何错误。

// Defining functions 
let x = (a: number) => { console.log(a); };
let y = (b: number, s: string) => { console.log(b + s); };

y = x; // OK
// x = y; // Error: x does not accept two arguments.

类和类型兼容性

当我们比较两个类时,它只比较实例的成员。类构造函数和静态成员属于类本身,因此它们不包含在比较中。

示例

在此代码中,变量 'a' 和变量 'b' 具有相同的实例成员。因此,当我们将变量 'a' 赋值给 'b' 时,它不会引发任何错误。

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number;
  constructor(meters: number) {}
}

let a: Animal;
let s: Size;
// Works because both classes have the same shape (they have the same instance properties).
a = s;

您可以使用接口和泛型进行类型兼容性。对于函数,目标函数应该至少具有与源函数相同的参数。对于类的类型兼容性,它们应该具有相同的实例成员。

广告