Typescript performs "duck" typing in certain circumstances, such as when you are checking the validity of an argument to a function against an interface.
For example:
interface Named {
name: string
}
function printName(x: Named) {
return x.name;
}
const myVar = {
name: "John",
happy: "OK", // This extra key-value pair does not break our printName function
};
printName(myVar);
However, when you create a variable and define its type, an extra key-value pair will throw a type error:
const myVar: Named = { name: "Jim", extraVal: "Oops" } // The "extraVal" is not allowed.
1) Why does Typescript check for the exact match in the second instance, but not with the parameter passed to the function?
2) Are there other instances when duck-typing is used, and how can one tell these instances apart?
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!
In addition to @jcalz's amazing answer, there are two typescriptical ways to bypass the check:
const myVar: Named & any = { name: "Jim", extraVal: "Oops" };
const myVar: Named = { name: "Jim", extraVal: "Oops" } as Named;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With