简体   繁体   中英

TypeScript interfaces not enforcing properties when an object is assigned

I'm having a hard time making sense of TypeScript's rules for interfaces. I understand that the following block of code throws an error because the id property isn't defined in the interface:

interface Person {
   name: string;
}

let person: Person = { name: 'Jack', id: 209 }

But why does TypeScript not throw an error if I try to add the id property by assigning another object? Like this for instance:

const samePerson = { name: 'Jack', id: 209 };
person = samePerson;

The person object ends up getting the id property, even though it isn't defined in the interface.

Object types are generally open and extendible, and allow extra properties...

Object types in TypeScript are generally open and extendible. An object is a Person if and only if it has a name property of type string . Such an object may turn out to have additional properties, but it's still a Person . This is very useful and allows interface extensions to form type hierarchies:

interface AgedPerson extends Person {
  age: number;
}

const agedPerson: AgedPerson = { name: "Alice", age: 35 };
const stillAPerson: Person = agedPerson; // okay

And because TypeScript has a structural type system , you don't actually have to declare an interface for AgedPerson for it to be seen as a subtype of Person :

const undeclaredButStillAgedPerson = { name: "Bob", age: 40 };
const andStillAPersonToo: Person = undeclaredButStillAgedPerson; // okay

Here undeclaredButStillAgedPerson has a type of {name: string, age: number} , equivalent to AgedPerson , and the subsequent assignment to a Person works for the same reason.


Even though open/extendible typing is useful, it can be confusing and is sometimes not desired. There is a longstanding open request at microsoft/TypeScript#12936 for TypeScript to support so-called exact types, where something like Exact<Person> would only be allowed to have a name property and nothing else. An AgedPerson would be a Person but not an Exact<Person> . There is currently no direct support for such exact types.


...but object literals do undergo excess property checking.

Jumping back: object types in TypeScript are generally open. But there is one situation where an object will be treated as if its type were exact. And this is when you assign an object literal to a variable or pass it as an argument.

Object literals get special treatment and undergo excess property checking when first assigned to a variable or passed as a function argument. If the object literal has properties not known to exist in the expected type, there's an error. Like this:

let person: Person = { name: 'Jack', id: 209 }; // error!
// ------------------------------->  ~~~~~~~
// Object literal may only specify known properties, 
// and 'id' does not exist in type 'Person'.

Even though {name: "Jack", id: 209} is a Person by the original definition, it's not an Exact<Person> , and so we get an error. Please note that the error specifically mentions "object literals".


Contrast this to the following, where there is no error:

const samePerson = { name: 'Jack', id: 209 }; // okay
person = samePerson; // okay

The assignment of the object literal to samePerson is not in error because samePerson 's type is inferred to be of the type

/* const samePerson: {
    name: string;
    id: number;
} */

and there's no excess property there. The subsequent assignment of samePerson to person also succeeds, because samePerson is not an object literal, and therefore excess property checking does not apply.


Playground link to code

Let's try and understand

An interface is a syntactical contract that an entity should conform to.

Interfaces define properties, methods, and events, which are the members of the interface. Interfaces contain only the declaration of the members. It is the responsibility of the deriving class to define the members. It often helps in providing a standard structure that the deriving classes would follow

In addition, we can implement multiple interfaces so that implementer can have more than what one interface defines.

While classes must adhere to what's in the contract, it can also have it's own additional implementation (This is mostly true with many languages)

Coming to Typescript, like @zerkms said

Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members.

The basic rule for TypeScript's structural type system is that x is compatible with y if y has at least the same members as x. For Example:

interface Pet {
  name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog;

To check whether dog can be assigned to pet, the compiler checks each property of pet to find a corresponding compatible property in dog. In this case, dog must have a member called name that is a string. It does, so the assignment is allowed.

Ref: type-compatibility

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM