为啥 TypeScript 中的类允许使用鸭子类型

Posted

技术标签:

【中文标题】为啥 TypeScript 中的类允许使用鸭子类型【英文标题】:Why duck typing is allowed for classes in TypeScript为什么 TypeScript 中的类允许使用鸭子类型 【发布时间】:2018-07-27 12:54:57 【问题描述】:

看起来在 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());

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

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

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

【问题讨论】:

因为安德斯允许。我不知道他会不会路过并告诉你为什么。 @MuratK。不,如果您将返回类型更改为 number 它仍然可以编译。 是的,我查过了。如果返回类型明显相同,它就可以工作 【参考方案1】:

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

但是,您的问题只会出现在普通类上,一旦您添加私有,即使它们具有相同的结构,类也会变得不兼容:

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

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

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


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

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

【讨论】:

@ivan 我认为打字稿会受益于“不可撤销的类型”......当你打开提案时告诉我,我会支持 :) @JonasW。我很确定有一个,同时,简单的解决方案是添加一个私有的,你甚至不必使用它,所以没有运行时惩罚,class Task private unduckable : true 就足够了,运行时@987654326 @ 将不存在 @titian 我知道,但这是一个糟糕的解决方法。我使用例如type userID = stringtype groupID = string 区分两者,因为它们非常相似。防止鸭子在那里打字会很好。 @JonasW。 typescript 编译器团队做了类似的事情,这就是我想到添加属性以使类型不兼容的地方。来自 TS 编译器代码:export type Path = string & __pathBrand: any ;github.com/Microsoft/TypeScript/blob/master/src/compiler/… 为了清楚起见:结构类型和 Duck 类型或多或少是一回事,这两者的对比类型系统称为名义类型:en.wikipedia.org/wiki/Nominal_type_system【参考方案2】:

现在可以使用 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 中的类允许使用鸭子类型的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 Typescript 对象允许这个额外的属性?

为啥 Typescript 允许将“任何”对象类型分配给类对象?

前端和鸭子有什么关系?

为什么TypeScript中的类允许使用duck typing

为啥 Typescript 允许将 `a: 1, b: 2` 分配给类型 `a: any | b:任何`? [复制]

多态及鸭子类型