共计 3527 个字符,预计需要花费 9 分钟才能阅读完成。
类型兼容:结构化类型
TypeScript 是一种基于 JavaScript 的动态类型语言,它为 JavaScript 增加了类型零碎,并提供了弱小的类型检查和主动补全性能。TypeScript 的类型零碎有一个十分重要的个性,那就是 “ 鸭子类型 ”(Duck Typing)或 “ 结构化类型 ”(Structural Typing)(文章会以 ” 鸭子类型 ”(Duck Typing)作为简称)。这种个性有时会让人感到诧异,但它是 TypeScript 加强 JavaScript 开发体验的重要形式之一。
鸭子类型的概念来自一个古老的英语成语:“如果它走起路来像一只鸭子,叫起来也像一只鸭子,那么它就是一只鸭子。”在 TypeScript(或更一般地说,动态类型语言)的上下文中,鸭子类型意味着一个对象的类型不是由它继承或实现的具体类别决定的,而是由它具备的构造决定的。
本文将全面深刻地探讨 TypeScript 中的鸭子类型,以及如何在理论的开发中利用和利用鸭子类型。
1. 鸭子类型:定义和示例
鸭子类型的概念来自一个古老的英语成语:“如果它走起路来像一只鸭子,叫起来也像一只鸭子,那么它就是一只鸭子。”在 TypeScript(或更一般地说,动态类型语言)的上下文中,鸭子类型意味着一个对象的类型不是由它继承或实现的具体类别决定的,而是由它具备的构造决定的。
这是一个简略的鸭子类型示例:
interface Duck {walk: () => void;
quack: () => void;}
function doDuckThings(duck: Duck) {duck.walk();
duck.quack();}
const myDuck = {walk: () => console.log('Walking like a duck'),
quack: () => console.log('Quacking like a duck'),
swim: () => console.log('Swimming like a duck')
};
doDuckThings(myDuck); // OK
在这个例子中,咱们定义了一个 Duck
接口和一个 doDuckThings
函数,这个函数须要一个 Duck
类型的参数。而后咱们创立了一个 myDuck
对象,它有 walk
、quack
和 swim
这三个办法。只管 myDuck
并没有显式地申明它实现了 Duck
接口,然而因为 myDuck
的构造满足了 Duck
接口的要求(即 myDuck
有 walk
和 quack
这两个办法),咱们能够将 myDuck
作为参数传递给 doDuckThings
函数。
这就是鸭子类型的基本概念:只有一个对象的构造满足了接口的要求,咱们就能够把这个对象看作是这个接口的实例,而不论这个对象的理论类型是什么。
2. 鸭子类型的长处
鸭子类型有许多长处,特地是在编写更灵便和更通用的代码方面。
2.1 代码的灵活性
鸭子类型减少了代码的灵活性。咱们能够创立和应用满足特定接口的任何对象,而不用放心它们的具体类型。这使得咱们能够更容易地编写通用的代码,因为咱们的代码只依赖于对象的构造,而不是对象的具体类型。
2.2 代码的复用
鸭子类型有助于代码的复用。因为咱们的函数和办法只依赖于对象的构造,咱们能够在不同的上下文中重用这些函数和办法,只有传入的对象满足所需的构造。
例如,咱们能够写一个函数,它承受一个具备 toString
办法的任何对象,而后返回这个对象的字符串示意。因为简直所有的 JavaScript 对象都有 toString
办法,咱们能够在许多不同的上下文中重用这个函数。
function toString(obj: { toString: () => string }) {return obj.toString();
}
console.log(toString(123)); // "123"
console.log(toString([1, 2, 3])); // "1,2,3"
console.log(toString({ a: 1, b: 2})); // "[object Object]"
2.3 与 JavaScript 的互操作性
鸭子类型进步了 TypeScript 与 JavaScript 的互操作性。因为 JavaScript 是一种动静类型语言,咱们常常须要解决的对象可能没有明确的类型。鸭子类型使咱们可能在 TypeScript 中平安地解决这些对象,只有它们的构造满足咱们的需要。
例如,咱们可能从一个 JavaScript 库获取一个对象,这个对象有一个 forEach
办法。咱们不关怀这个对象的具体类型,咱们只关怀它是否有 forEach
办法。应用鸭子类型,咱们能够定义一个接口来形容这个对象的构造,而后在 TypeScript 中平安地应用这个对象。
interface Iterable {forEach: (callback: (item: any) => void) => void;
}
function processItems(iterable: Iterable) {iterable.forEach(item => console.log(item));
}
const jsArray = [1, 2, 3]; // From a JavaScript library
processItems(jsArray); // OK
3. 鸭子类型的局限性
只管鸭子类型有许多长处,但它也有一些局限性。
3.1 类型平安
鸭子类型可能会升高代码的类型安全性。因为 TypeScript 的类型查看器只查看对象是否满足接口的构造,而不查看对象是否真的是接口所冀望的类型。如果一个对象恰好有与接口雷同的属性和办法,但实际上它并不是接口所冀望的类型,TypeScript 的类型查看器可能无奈发现这个谬误。
例如,咱们可能有一个 Dog
类型和一个 Cat
类型,它们都有一个 bark
办法。咱们可能会谬误地将一个 `Cat
对象传递给一个冀望
Dog` 对象的函数,而 TypeScript 的类型查看器无奈发现这个谬误。
interface Dog {bark: () => void;
}
function letDogBark(dog: Dog) {dog.bark();
}
const cat = {bark: () => console.log('Meow...'), // Cats don't bark!
purr: () => console.log('Purr...')
};
letDogBark(cat); // No error, but it's wrong!
在这种状况下,咱们须要更认真地设计咱们的类型和接口,以防止混同。
3.2 易读性和可维护性
鸭子类型可能会升高代码的易读性和可维护性。因为咱们的代码只依赖于对象的构造,而不是对象的具体类型,这可能会使代码更难了解和保护。
为了进步易读性和可维护性,咱们须要清晰地记录咱们的接口和函数冀望的对象构造。TypeScript 的类型注解和接口提供了一种弱小的工具来实现这一点。
4. 应用鸭子类型的最佳实际
在应用鸭子类型时,有一些最佳实际能够帮忙咱们防止上述问题,并充分利用鸭子类型的长处。
4.1 清晰地定义接口
咱们应该清晰地定义咱们的接口,以形容咱们的函数和办法冀望的对象构造。这有助于进步代码的易读性和可维护性。
例如,如果咱们有一个函数,它冀望一个具备 name
和 age
属性的对象,咱们应该定义一个接口来形容这个构造。
interface Person {
name: string;
age: number;
}
function greet(person: Person) {console.log(`Hello, my name is ${person.name} and I'm ${person.age} years old.`);
}
4.2 适度应用鸭子类型
咱们应该适度地应用鸭子类型。尽管鸭子类型有许多长处,但如果适度应用,可能会导致类型安全性的问题,以及易读性和可维护性的升高。咱们应该在类型安全性、易读性、可维护性和灵活性之间找到一个均衡。
在某些状况下,咱们可能更心愿应用类和继承,而不是鸭子类型。例如,如果咱们有一组严密相干的类型,它们有共享的行为和状态,应用类和继承可能更适合。
interface Named {name: string;}
class Person {
name: string;
constructor(name: string) {this.name = name;}
}
let p: Named;
// OK, because of structural typing
p = new Person('mike');
在这个例子中,只管 Person
类并没有显式地实现 Named
接口,然而因为 Person
类有一个 name
属性,所以咱们能够把 Person
的实例赋值给 Named
类型的变量。这是因为 TypeScript 的 “ 鸭子类型 ” 或 “ 结构化类型 ” 零碎导致的。