Why Partial accepts extra properties from another type?
Asked Answered
E

1

11

Given interfaces A and B, which contain a x1 property in common

interface A {
  a1: number;
  a2: number;
  x1: number;  // <<<<
}

interface B{
  b1: number;
  x1: number;  // <<<<
}

And given the implementations a and b

let a: A = {a1: 1, a2: 1, x1: 1};
let b: B = {b1: 1, x1: 1};

This simply passes, even though b1 does not belong to Partial<A>:

let partialA: Partial<A> = b;

but this fails because b1 does not belong to Partial<A>:

let partialA: Partial<A> = {b1: 1, x1: 1};

Could someone please tell me why?

Eyeshot answered 12/6, 2019 at 10:5 Comment(2)
#31816561Insignificancy
@Insignificancy excess property checks is part of it, but if the types did not have anythingn in common this would be an errorAbutting
A
7

This is going to be a bit of a journey so hang in there with me:

Generally a sub-type should be assignable to a base-type. In Typescript a type with more properties should be assignable to a where a type with just a subset of the properties is expected. So this for example is legal:

let source = { a1: 0, a2: 0}
let target: { a1: number } = source

Now surprisingly, because of the way structural typing works Partial<A> is a subtype of Partial<B> and Partial<B> is a subtype of Partial<A>. Optional properties can be missing in a subtype, so optional properties missing from a type do not disqualify a type from being a subtype. An if we remove the optional properties we are just left with {} which can be a the base type of any other type. The compiler agrees with me on this point if we ask it to answer this subtyping question using conditional types:

type q1 = Partial<A> extends Partial<B> ? "Y" : "N" // Y
type q2  = Partial<B> extends Partial<A> ? "Y" : "N" // Y

There is one exception to this (ok maybe two), assignment of object literals directly to a reference of a particular type. This is called excess property checks as is the reason that if we were to perform the above assignment directly we would get an error:

let target: { a1: number } = { a1: 0, a2: 0} // err  

The reason this is an error is that it is a common error to mistakenly create an object literal with more properties or misspelled properties and this check (which is a violation of the principle a sub-type should be assignable to a base-type) is there to catch this mistake. This is also the reason you are getting an error on

let partialA: Partial<A> = {b1: 1, x1: 1};

But excess property checks only kick in on direct assignment of an object literal to variable of a specific type. So on the assignment let partialA: Partial<A> = b; it will not trigger an error on excess property checks.

One further complication is that Partial<A> is what is called a weak type. From the PR introducing checks for such type:

A weak type is one which has only optional properties and is not empty. Because these types are assignable from anything except types with matching non-assignable properties, they are very weakly typechecked. The simple fix here is to require that a type is only assignable to a weak type if they are not completely disjoint.

Now since a weak type has no mandatory properties, following the principle a sub-type should be assignable to a base-type, any other object would be assignable to such a type:

let k = { foo: 0 }
let partialA: Partial<A> = k;

The type of k is a sub-type of Partial<A>, sure k has nothing in common with Partial<A> but that is not an issue. After all Partial<A> does not require any propertie, so k does not need any, and it has one extra property, which is generally what a sub-type does, become more specific by adding members.

The reason the code code above is an error is because of the PR are reference above which postulates that assigning a completely unrelated type to a weak type is probably an error. So like excess property checks a rule was introduced (a rule which again breaks the idea a subtype is assignable to a base type) that disallows such assignments.

In your case however Partial<A> and Partial<B> do have something in common x1 so this rule does not catch the possibly mistaken assignment. Although surprising because both Partial<A> and Partial<B> have no mandatory properties, they are subtypes of one another and unless there is a specific reason (such as excess property checks or this extra weak type detection rule) the assignment is allowed.

Abutting answered 12/6, 2019 at 10:45 Comment(2)
would there be an alternative to Partial to accept only fields from another type and nothing else (not requiring all the fields though)?Eyeshot
@DouglasCarvalhoVasconcelos on assignment I don't think so, not without something like negated types which we currently do not have. If you have function and you want to forbid extra properties, that can be done. Maybe post another question for that ? This question was about why, and the answer is already very long.Abutting

© 2022 - 2024 — McMap. All rights reserved.