Ryan Cavanaugh
Ryan Cavanaugh

Reputation: 221014

Why does TypeScript only sometimes treat an impossible intersection as 'never'?

TypeScript will sometimes decide that two types, if intersected, don't have any compatible values. This empty intersection is called never and means that you can't provide a value that fulfills both types:

type Bread = {
  shape: "loafy"
};
type Car = {
  shape: "carish"
};

// Contradiction1: Immediately resolved to 'never'
type Contradiction1 = Bread & Car;

However, this seems to work inconsistently. If the conflicting properties are not at the top level of the type, TypeScript misses it and doesn't behave the way I expect:

// Wrap the contradicting types
type Garage = { contents: Car };
type Breadbox = { contents: Bread };

// Contradiction2: Garage & Breadbox
// Expected: Should immediately reduce to never
type Contradiction2 = Garage & Breadbox;

Is this a bug? Why does TypeScript behave this way?

Upvotes: 2

Views: 69

Answers (1)

Ryan Cavanaugh
Ryan Cavanaugh

Reputation: 221014

This is the intended behavior because property paths in TypeScript can go arbitrarily deep while changing types along the way. For example, it's perfectly legal to write something like this:

declare class Boxed<T> {
  contents: T;
  doubleBoxed: Boxed<this>
};
declare const b: Boxed<string>
// m: Boxed<Boxed<Boxed<Boxed<Boxed<string>>>>>
const m = b.doubleBoxed.doubleBoxed.doubleBoxed.doubleBoxed;

So already for an arbitrary type, there's an effectively infinite number of properties that "can" exist, any of which could have some novel type never seen before in your program.

This matters for never because you might write something like this:

// I am here to cause trouble.
type M<T, S> = T extends { nested: { nested: { nested: any } } } ?
  S :
  { el: T, nested: M<{ nested: T }, S> };

type F1 = {
  prop: M<F1, "foo">
};
type F2 = {
  prop: M<F2, "bar">
};

declare const f1: F1;
// f1.prop.nested.nested.nested: "foo"
f1.prop.nested.nested.nested;

declare const f12: F1 & F2;

// OK, infinitely
f12.prop.el.prop.el.prop.el.prop.el.prop;
// 'never' at depth 4...
f12.prop.nested.nested.nested;

There's really no way to predict where in you would need to go looking to find expressions that might result in never - the definition of M didn't give us any hints; you have to really understand this code as a human to know where to explore to find a nested never.

In fact, if you could resolve this "correctly" for any arbitrary depth of property access, you could do things like prove/disprove the Collatz conjecture by structuring types that perform arithmetic (which is already possible). Clearly this isn't possible, so TypeScript doesn't try beyond the easily-resolved case of the top-level properties of the produced type.

Upvotes: 4

Related Questions