Why Duck Typing Is Allowed for Classes in Typescript

Why duck typing is allowed for classes in TypeScript

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: Misunderstanding about duck typing

Typescript enforces different levels of strictness depending on what you're doing. When you create a variable with an explicit type, typescript is very strict about what must be there. You must exactly follow the type definition. Anything else is very likely a bug in your code, so by being as strict as possible it helps you to catch bugs.

When you try to assign one variable to another, including passing a variable into a function, typescript does a looser check. The two variables just need to be compatible with eachother, which roughly means they need to have at least the listed properties with the right types, but it's ok if it also has more properties. This looser level of checking is important to support subtypes and subclasses. You should be able to pass a subclass in where a baseclass is called for (the liskov substitution principle), but the subclass will often have extra properties

See this link for more information on type compatibility: https://www.typescriptlang.org/docs/handbook/type-compatibility.html

How is it possible to duck type an object when the Class has encapsulated properties?

Privates make the class behave nominally, it not possible to alias a class with private fields. We can on the other hand remove them by using Pick to get only the public properties:

let blogPostItem: Pick<BlogPost, keyof BlogPost>;

blogPostItem = {
hoverTitle: '',
summary: '',
title: ''
}

You can also use a type assertion to ignore the errors.

Duck Typing vs. Function Arguments in Typescript

TypeScript's type system is structural (which you're calling "duck" typing), and in general, extra properties are not considered to violate the structure of an object type. In other words, object types in TypeScript are "open/extendible" and not "closed/exact"; the type {a: string} is known to have a string-valued a property, but is not known to lack other properties.

Open object types enable useful things like interface and class extension, so if Y extends X then you can use a Y everywhere you could use an X, even if Y has more functionality.

So to answer your second question, most places in the language rely only on structural subtyping.


As far as I know, the only place where the compiler acts as if object types were exact is when you create a new object literal. The compiler assumes that when you create an object literal that you care about all its properties. If you then immediately assign such a literal to a type that does not know about all the object literal's properties, the compiler warns you: the compiler will forget about these extra properties and be unable to track them, and it might be a mistake on your part. This is called excess property checking. It only kicks in when you have a "fresh" object literal (that has not yet been assigned anywhere) and you assign it to a type that does not expect all its properties.

The example given in the handbook for why this check is desirable involves misspelling optional properties. If you have a variable of a type like { weird?: boolean } and assign to it the object literal { wierd: true }, the compiler says "hmm, this value does fit the type. It has no weird property, which is fine because it's optional. But it has this extra wierd property that I'm going to immediately forget about; why would someone do that? Maybe that's an error." I don't know whether you agree with this reasoning or not, but there it is.


So to answer your first question, the compiler is happy with

const myVar = {
name: "John",
happy: "OK"
};
printName(myVar);

because the object literal is not widened in its initial assignment (the type of myVar is known to have both a name and a happy property), and by the time you pass it into printName(), it's no longer "fresh". The compiler will not know about the happy property inside the implementation of printName(), but it does know about the happy property in myVar.

And it's unhappy with

const myVar: Named = { name: "Jim", happy: "OK" };

because it gets caught by excess property checking. The type of myVar will not contain any reference to happy.


Okay, hope that helps; good luck!

How to handle duck-typed union types to TypeScript interfaces?

You're almost there. Few things to fix:

  1. In order to create instance of type you should use new keyword - new duckTypeMap[x.type];
  2. In order to initialize this instance's fields you should create copy constructor or just map json object (manually or with some library). Have a look at this answer for example.
  3. If your class implements the interface it should declare members of this interface. Also not sure what you gain from using union type. In order to have polymorphic array you could just define single interface IEvent with when property and implement it in all classes.

Something like this:

interface IEvent {
when: string
}

class PaymentEvent implements IEvent {
public amount:string;
public when:string;
}

const typedTimeline:IEvent[] = timeline.map(x => {
let target = new duckTypeMap[x.type];
for (const key in x) {
target[key] = x[key];
}
return target;
});

To determine item type when you iterate over the "typed" array you can use instanceof operator:

if(item instanceof RefundedEvent)


Related Topics



Leave a reply



Submit