29

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.

Ivan Velichko
  • 6,348
  • 6
  • 44
  • 90

2 Answers2

28

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.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Flexibility is awesome, but now I'm going to add a random private attribute for all my classes which are not supposed to be used interchangable. – Ivan Velichko Feb 16 '18 at 15:36
  • IMO, If I defined a function like this `foo(t: { run(): void })` it should mean that the function accepts an anonymous `Runnable` interface. And it does in TypeScript. But if I defined a function like `foo(t: MyClass)` then it should accept only instances of `MyClass` or its successor. – Ivan Velichko Feb 16 '18 at 15:43
  • 2
    @ivan i think that typescript would benefit from "unduckable types" ... tell me when you open a proposal, i will support that :) – Jonas Wilms Feb 16 '18 at 15:45
  • @IvanVelichko It seems frustrating at first, I know. But this is the way JS works, and the goal of TS is to let you do anything you can do in JS in a type safe way, if it were to function any other way it would severely limit what you can do compared to JS – Titian Cernicova-Dragomir Feb 16 '18 at 15:46
  • 1
    @JonasW. I'm pretty sure there is one, in the mean time, the simple solution is to add a private, you don't even have to use it, so there is no runtime penalty, `class Task { private unduckable : true }` is enough, at runtime `unduckable` will not exist – Titian Cernicova-Dragomir Feb 16 '18 at 15:48
  • @TitianCernicova-Dragomir I hope that you're joke. This behavior is very implicit. And it's uncomfortable. – dlarchikov Feb 16 '18 at 16:04
  • @dimka3210 It's a work around to achieve a desired behavior, that TS was not really designed to accomplish, I never said it was comfortable :) – Titian Cernicova-Dragomir Feb 16 '18 at 16:06
  • 1
    @titian i know but that is a bad workaround. I use e.g. `type userID = string` , `type groupID = string` to distuinguish both as they are quite similar. And it would be nice to prevent the duck typing there. – Jonas Wilms Feb 16 '18 at 16:21
  • 4
    @JonasW. The typescript compiler team does something similar, and that is where I got the idea to add the properties to make types incompatible. From the TS compiler code : `export type Path = string & { __pathBrand: any };` https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts – Titian Cernicova-Dragomir Feb 16 '18 at 16:49
  • @titian thanks for the hint... I will definetly consider that – Jonas Wilms Feb 16 '18 at 16:50
  • 1
    Just for sake of clarity: Structural typing and Duck typing are more or less the same thing, and the contrasting type system of these two is called Nominal typing: https://en.wikipedia.org/wiki/Nominal_type_system – Sámal Rasmussen Mar 21 '18 at 14:07
6

It is now possible to create nominal types with TypeScript that allow you to discriminate types by context. Please consider the following question:

Atomic type discrimination (nominal atomic types) in TypeScript

With it's example:

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
Lu4
  • 14,873
  • 15
  • 79
  • 132