Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Duck" Typing vs. Function Arguments in Typescript

Tags:

typescript

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?

like image 908
Harrison Cramer Avatar asked Dec 06 '25 14:12

Harrison Cramer


2 Answers

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!

like image 197
jcalz Avatar answered Dec 08 '25 20:12

jcalz


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;
like image 31
glinda93 Avatar answered Dec 08 '25 18:12

glinda93



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!