Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does TypeScript's Structural Typing (i.e. "Duck Typing") necessitate non-strict Type Unions? [duplicate]

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?

like image 383
Joseph Morgan Avatar asked Oct 16 '25 19:10

Joseph Morgan


1 Answers

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.)

like image 150
Josh Kelley Avatar answered Oct 18 '25 14:10

Josh Kelley



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!