Assume that the following types are defined:
type A = {
a: string,
}
type B = {
a: number,
b: number,
}
These two assignments produce an error:
// Not assignable to A because 'b' doesn't exist on A
const x: A = {
a: 'hello',
b: 10
}
// Not assignable to B because 'a' expects type 'string'
const x: B = {
a: 'hello',
b: 10
}
TypeScript's documentation defines a Union Type as:
A union type is a type formed from two or more other types, representing values that may be any one of those types.
Based on this, I would have expected the following declaration to produce an error:
const x: A | B = {
a: 'hello',
b: 10
}
But it doesn't. The object literally is not assignable to type A
and it's not assignable to type B
, yet it is assignable to A | B
.
How can I think of Type Unions in a way that makes this result make sense?
Editing: Of course, I search for half the day and can't find information that documents this, but as soon as I post, I find a number of other posts here talking about the same thing.
From what I found, an object literal can have excess properties when assigned as a Type Union, as long as one of the members has that property.
If I understand correctly, my object literal satisfies type A
, with the property b
being an excess property, which is allowed because another member of the union (B
) includes the property.
Why does TypeScript's Structural Typing (i.e. "Duck Typing") necessitate non-strict Type Unions? From the number of SO posts I'm seeing relating to this, it seems like most people expect a Type Union to be strict. I'm sure that there's a compelling reason for non-strict unions to be the default, can someone provide some intuition for why that's the case?
From a type theory perspective, TypeScript uses structural typing ("if it acts like a duck, it's a duck") and allows additional properties. As a result:
const x = {
a: 'hello',
b: 10
}
function doSomethingWithA(a: A) {}
// Works: x has shape type A = { a: string }
doSomethingWithA(x);
// Also works: Same reason
const y: A = x;
Your A | B
assignment works for similar reasons:
// Works: has shape { a: string } | { a: number, b: number }
const x: A | B = {
a: 'hello',
b: 10
}
TypeScript has additional handling for the specific case of object literals with an explicit type defined: since that's a common potential source of errors, it raises errors for that specific case. See Microsoft/TypeScript/#3755, which offers this more in-depth explanation from Anders Hejlsberg for the rationale:
An interesting fact about object literals (and array literals) is that an object reference produced by a literal is known to be the only reference to that object. Thus, when an object literal is assigned to a variable or passed for a parameter of a type with fewer properties than the object literal, we know that information is irretrievably lost. This is a strong indication that something is wrong--probably stronger than the benefits afforded by allowing it.
There's a popular feature request for exact types, which could help enforce all of this more strictly, but it has not been implemented.
Now, if I understand correctly, TypeScript could extend its "not assignable" special case from Microsoft/TypeScript/#3755 to union literals: it presumably hasn't due to some combination of lack of demand, implementation complexity, or compiler performance hit. (I haven't searched GitHub issues to see if this has been requested.)