I have recently stumbled upon this weird (imo) behavior in TypeScript. During compilation it will complain about excess properties only if the expected variable's type is an interface if the interface has no mandatory fields. Link to TypeScript Playground #1: http://goo.gl/rnsLjd
interface IAnimal {
name?: string;
}
class Animal implements IAnimal {
}
var x : IAnimal = { bar: true }; // Object literal may only specify known properties, and 'bar' does not exist in type 'IAnimal'
var y : Animal = { bar: true }; // Just fine.. why?
function foo<T>(t: T) {
}
foo<IAnimal>({ bar: true }); // Object literal may only specify known properties, and 'bar' does not exist in type 'IAnimal'
foo<Animal>({ bar: true }); // Just fine.. why?
Now, if you add a 'mandatory' field to the IAnimal interface and implement it in the Animal class it will start complaining about 'bar' being an excess property for bot interfaces and classes. Link to TypeScript Playground #2: http://goo.gl/9wEKvp
interface IAnimal {
name?: string;
mandatory: number;
}
class Animal implements IAnimal {
mandatory: number;
}
var x : IAnimal = { mandatory: 0, bar: true }; // Object literal may only specify known properties, and 'bar' does not exist in type 'IAnimal'
var y : Animal = { mandatory: 0, bar: true }; // Not fine anymore.. why? Object literal may only specify known properties, and 'bar' does not exist in type 'Animal'
function foo<T>(t: T) {
}
foo<IAnimal>({ mandatory: 0, bar: true }); // Object literal may only specify known properties, and 'bar' does not exist in type 'IAnimal'
foo<Animal>({ mandatory: 0,bar: true }); // Not fine anymore.. why? Object literal may only specify known properties, and 'bar' does not exist in type 'Animal'
If anyone has some insights as to why that works as it does please do.
I am very curious as to why that is.
The following three bullet points from pull request shed a bit of light on the new strict behavior in TS 1.6 which is used in the playground:
- Every object literal is initially considered "fresh".
- When a fresh object literal is assigned to a variable or passed for a parameter of a non-empty target type [emphasis added], it is an error for the object literal to specify properties that don't exist in the target type.
- Freshness disappears in a type assertion or when the type of an object literal is widened.
I have found in the source code function hasExcessProperties
and function isKnownProperty
with the comment:
// Check if a property with the given name is known anywhere in the given type. In an object type, a property // is considered known if the object type is empty and the check is for assignability, if the object type has // index signatures, or if the property is actually declared in the object type. In a union or intersection // type, a property is considered known if it is known in any constituent type. function isKnownProperty(type: Type, name: string): boolean { if (type.flags & TypeFlags.ObjectType) { const resolved = resolveStructuredTypeMembers(type); if (relation === assignableRelation && (type === globalObjectType || resolved.properties.length === 0) || resolved.stringIndexType || resolved.numberIndexType || getPropertyOfType(type, name)) { return true; } } else if (type.flags & TypeFlags.UnionOrIntersection) { for (const t of (<UnionOrIntersectionType>type).types) { if (isKnownProperty(t, name)) { return true; } } } return false; }
So target type Animal
(the class) in your first example is an empty type - it has no properties because you did not implement name
property in the class (therefore resolved.properties.length === 0
is true in isKnownProperty
function). On the other hand IAnimal
has properties defined.
I may have described the behavior a bit technically but ... hopefully, I made it clear and hopefully, I did not make a mistake along the road.
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