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:
- In order to create instance of type you should use
new
keyword -new duckTypeMap[x.type];
- 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.
- 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 interfaceIEvent
withwhen
property and implement it in all classes.
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
How to Set Node_Env to Production/Development in Os X
Load Local JSON File into Variable
Can Promises Have Multiple Arguments to Onfulfilled
Decompress Gzip and Zlib String in JavaScript
Sampling a Random Subset from an Array
Remove Zero-Width Space Characters from a JavaScript String
How to Copy a Dom Node with Event Listeners
Why Is 'Event' Variable Available Even When Not Passed as a Parameter
How to Convert Numbers Between Different Bases in JavaScript
How to Auto-Reload Files in Node.Js
Do We Need Semicolon at the End
Obtain Smallest Value from Array in JavaScript
How to Randomly Generate HTML Hex Color Codes Using JavaScript
How to Pass a Flag to Gulp to Have It Run Tasks in Different Ways
Different Result for Yyyy-Mm-Dd and Yyyy/Mm/Dd in JavaScript When Passed to "New Date"
Invoke a Callback at the End of a Transition
JavaScript - Generating All Combinations of Elements in a Single Array (In Pairs)