为什么在 TypeScript 中 类 允许鸭子打字
Why duck typing is allowed for classes in TypeScript
看起来在 TypeScript 中拥有这样的代码绝对没问题(从编译器的角度来看):
class Vehicle {
public run(): void { console.log('Vehicle.run'); }
}
class Task {
public run(): void { console.log('Task.run'); }
}
function runTask(t: Task) {
t.run();
}
runTask(new Task());
runTask(new Vehicle());
但同时我预计会出现 编译错误,因为 Vehicle
和 Task
没有任何共同点。
和合理的用法可以通过显式接口定义来实现:
interface Runnable {
run(): void;
}
class Vehicle implements Runnable {
public run(): void { console.log('Vehicle.run'); }
}
class Task implements Runnable {
public run(): void { console.log('Task.run'); }
}
function runRunnable(r: Runnable) {
r.run();
}
runRunnable(new Task());
runRunnable(new Vehicle());
...或一个共同的父对象:
class Entity {
abstract run(): void;
}
class Vehicle extends Entity {
public run(): void { console.log('Vehicle.run'); }
}
class Task extends Entity {
public run(): void { console.log('Task.run'); }
}
function runEntity(e: Entity) {
e.run();
}
runEntity(new Task());
runEntity(new Vehicle());
是的,对于 JavaScript 来说,有这样的行为绝对没问题,因为没有 classes 也根本没有编译器(只有语法糖)并且鸭子类型对于该语言来说是自然的.但是 TypeScript 试图引入静态检查、classes、接口等。然而,在我看来,class 实例的鸭子类型看起来相当混乱且容易出错。
这就是结构类型的工作方式。 Typescript 有一个结构类型系统,可以最好地模拟 Javscript 的工作方式。由于 Javascript 使用鸭子类型,任何定义契约的对象都可以在任何函数中使用。 Typescript 只是尝试在编译时而不是在运行时验证鸭子类型。
然而,您的问题只会出现在琐碎的 classes 中,一旦您添加了 privates,即使它们具有相同的结构,classes 也会变得不兼容:
class Vehicle {
private x: string;
public run(): void { console.log('Vehicle.run'); }
}
class Task {
private x: string;
public run(): void { console.log('Task.run'); }
}
function runTask(t: Task) {
t.run();
}
runTask(new Task());
runTask(new Vehicle()); // Will be a compile time error
此行为还允许您不显式实现接口,例如,您的函数可以为内联参数定义接口,并且任何满足约定的 class 将是兼容的,即使它们没有显式实现实现任何接口:
function runTask(t: { run(): void }) {
t.run();
}
runTask(new Task());
runTask(new Vehicle());
就我个人而言,从 C# 开始,这似乎很疯狂,但在可扩展性方面,这种类型检查方式提供了更大的灵活性,一旦你习惯了它,你就会看到它的好处。
现在可以使用 TypeScript 创建名义类型,允许您根据上下文区分类型。请考虑以下问题:
Atomic type discrimination (nominal atomic types) in TypeScript
以它为例:
export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };
export interface MetricWeight {
value: Kilos<number>
}
export interface ImperialWeight {
value: Pounds<number>
}
const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }
wm.value = wi.value; // Gives compiler error
wi.value = wi.value * 2; // Gives compiler error
wm.value = wi.value * 2; // Gives compiler error
const we: MetricWeight = { value: 0 } // Gives compiler error
看起来在 TypeScript 中拥有这样的代码绝对没问题(从编译器的角度来看):
class Vehicle {
public run(): void { console.log('Vehicle.run'); }
}
class Task {
public run(): void { console.log('Task.run'); }
}
function runTask(t: Task) {
t.run();
}
runTask(new Task());
runTask(new Vehicle());
但同时我预计会出现 编译错误,因为 Vehicle
和 Task
没有任何共同点。
和合理的用法可以通过显式接口定义来实现:
interface Runnable {
run(): void;
}
class Vehicle implements Runnable {
public run(): void { console.log('Vehicle.run'); }
}
class Task implements Runnable {
public run(): void { console.log('Task.run'); }
}
function runRunnable(r: Runnable) {
r.run();
}
runRunnable(new Task());
runRunnable(new Vehicle());
...或一个共同的父对象:
class Entity {
abstract run(): void;
}
class Vehicle extends Entity {
public run(): void { console.log('Vehicle.run'); }
}
class Task extends Entity {
public run(): void { console.log('Task.run'); }
}
function runEntity(e: Entity) {
e.run();
}
runEntity(new Task());
runEntity(new Vehicle());
是的,对于 JavaScript 来说,有这样的行为绝对没问题,因为没有 classes 也根本没有编译器(只有语法糖)并且鸭子类型对于该语言来说是自然的.但是 TypeScript 试图引入静态检查、classes、接口等。然而,在我看来,class 实例的鸭子类型看起来相当混乱且容易出错。
这就是结构类型的工作方式。 Typescript 有一个结构类型系统,可以最好地模拟 Javscript 的工作方式。由于 Javascript 使用鸭子类型,任何定义契约的对象都可以在任何函数中使用。 Typescript 只是尝试在编译时而不是在运行时验证鸭子类型。
然而,您的问题只会出现在琐碎的 classes 中,一旦您添加了 privates,即使它们具有相同的结构,classes 也会变得不兼容:
class Vehicle {
private x: string;
public run(): void { console.log('Vehicle.run'); }
}
class Task {
private x: string;
public run(): void { console.log('Task.run'); }
}
function runTask(t: Task) {
t.run();
}
runTask(new Task());
runTask(new Vehicle()); // Will be a compile time error
此行为还允许您不显式实现接口,例如,您的函数可以为内联参数定义接口,并且任何满足约定的 class 将是兼容的,即使它们没有显式实现实现任何接口:
function runTask(t: { run(): void }) {
t.run();
}
runTask(new Task());
runTask(new Vehicle());
就我个人而言,从 C# 开始,这似乎很疯狂,但在可扩展性方面,这种类型检查方式提供了更大的灵活性,一旦你习惯了它,你就会看到它的好处。
现在可以使用 TypeScript 创建名义类型,允许您根据上下文区分类型。请考虑以下问题:
Atomic type discrimination (nominal atomic types) in TypeScript
以它为例:
export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };
export interface MetricWeight {
value: Kilos<number>
}
export interface ImperialWeight {
value: Pounds<number>
}
const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }
wm.value = wi.value; // Gives compiler error
wi.value = wi.value * 2; // Gives compiler error
wm.value = wi.value * 2; // Gives compiler error
const we: MetricWeight = { value: 0 } // Gives compiler error