Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference in how TypeScript handles excess properties in interfaces and classes

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.

like image 259
DenisPostu Avatar asked Nov 09 '15 17:11

DenisPostu


1 Answers

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.

like image 96
Martin Vseticka Avatar answered Sep 20 '22 14:09

Martin Vseticka