为什么 TypeScript 中的类允许使用鸭子类型 [英] Why duck typing is allowed for classes in TypeScript

查看:27
本文介绍了为什么 TypeScript 中的类允许使用鸭子类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

看起来在 TypeScript 中(从编译器的角度来看)拥有这样的代码绝对没问题:

class Vehicle {public run(): void { console.log('Vehicle.run');}}类任务{public run(): void { console.log('Task.run');}}函数运行任务(t:任务){t.run();}运行任务(新任务());运行任务(新车辆());

但与此同时,我预计会出现编译错误,因为VehicleTask 没有任何共同点.

并且sane用法可以通过显式接口定义来实现:

interface Runnable {运行():无效;}类 Vehicle 实现 Runnable {public run(): void { console.log('Vehicle.run');}}类 Task 实现 Runnable {public run(): void { console.log('Task.run');}}函数运行运行(r:运行){r.run();}运行可运行(新任务());运行可运行(新车辆());

... 或一个共同的父对象:

class 实体 {抽象运行():无效;}类车辆扩展实体{public run(): void { console.log('Vehicle.run');}}类任务扩展实体{public run(): void { console.log('Task.run');}}函数运行实体(e:实体){e.run();}运行实体(新任务());运行实体(新车辆());

是的,对于 JavaScript 来说,拥有这样的行为绝对没问题,因为根本没有类和编译器(只有语法糖),并且鸭子类型对于该语言来说是很自然的.但是 TypeScript 试图引入静态检查、类、接口等.然而,在我看来,类实例的鸭子类型看起来相当混乱且容易出错.

解决方案

这是结构类型的工作方式.Typescript 有一个结构类型系统,可以最好地模拟 Javscript 的工作方式.由于 Javascript 使用鸭子类型,因此定义合约的任何对象都可以在任何函数中使用.Typescript 只是尝试在编译时而不是在运行时验证鸭子类型.

然而,您的问题只会出现在琐碎的类中,一旦您添加私有类,即使它们具有相同的结构,类也会变得不兼容:

class Vehicle {私有 x:字符串;public run(): void { console.log('Vehicle.run');}}类任务{私有 x:字符串;public run(): void { console.log('Task.run');}}函数运行任务(t:任务){t.run();}运行任务(新任务());运行任务(新车辆());//会出现编译时错误

这种行为还允许您不显式实现接口,例如您的函数可以为参数内联定义接口,任何满足契约的类即使没有显式实现任何接口也将兼容:

function runTask(t: { run(): void }) {t.run();}运行任务(新任务());运行任务(新车辆());

就个人而言,从 C# 开始,这似乎很疯狂,但是当涉及到可扩展性时,这种类型检查方式可以提供更大的灵活性,一旦您习惯了它,您就会看到好处.

Looks like in TypeScript it's absolutely fine (from the compiler perspective) to have such code:

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());

But at the same time I would expect a compilation error, because Vehicle and Task don't have anything in common.

And sane usages can be implemented via explicit interface definition:

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());

... or a common parent object:

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());

And yes, for JavaScript it's absolutely fine to have such behaviour, because there is no classes and no compiler at all (only syntactic sugar) and duck typing is natural for the language. But TypeScript tries to introduce static checks, classes, interfaces, etc. However duck typing for class instances looks rather confusing and error-prone, in my opinion.

解决方案

This is the way structural typing works. Typescript has a structural type system to best emulate how Javscript works. Since Javascript uses duck typing, any object that defines the contract can be used in any function. Typescript just tries to validate duck typing at compile time instead of at runtime.

Your problem will however only manifest for trivial classes, as soon as you add privates, classes become incompatible even if they have the same structure:

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

This behavior also allows you to not explicitly implement interfaces, for example you function could define the interface for the parameter inline, and any class that satisfies the contract will be compatible even if they don't explicitly implement any interface:

function runTask(t: {  run(): void }) {
    t.run();
}

runTask(new Task());
runTask(new Vehicle());

On a personal note, coming from C# this was seemed insane at first, but when it comes to extensibility this way of type checking allows for much greater flexibility, once you get used to it you will see the benefits.

这篇关于为什么 TypeScript 中的类允许使用鸭子类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆