Mister Epic
Mister Epic

Reputation: 16713

Inferring types in array

Given the following:

type ArrayInfer<A> = A extends (infer I)[] ? I : never;

const a = ["foo", 1, {a: "foo"}];
type inferred = ArrayInfer<typeof a>; 

My type inferred resolves to:

type inferred = string | number | {
    a: string;
}

But if I change my array slightly to include an empty object literal:

const a = ["foo", 1, {}];

Then inferred no longer includes string and number in its type definition:

type inferred = {}

Why is that?

Upvotes: 4

Views: 785

Answers (2)

jo_va
jo_va

Reputation: 13964

To extend @Cerberus' answer and as suggested by @Paleo in the comments, all types in JavaScript except Object.create(null), null and undefined have Object as their top ancestors as shown by the following code snippet:

const arr = [{}, 1, 'hello', (a, b) => a+b, true, null, undefined, Object.create(null)];

function getPrototypeChain(value) {
  const ancestors = [];
  let proto = value;
  while (proto) {
    proto = Object.getPrototypeOf(proto);
    if (proto) {
      ancestors.push(proto.constructor.name);
    }
  };
  return ancestors
}

arr.forEach(x => console.log(x, '->', getPrototypeChain(x).join(' -> ')));

From the TypeScript doc (Best Common Type, suggested by @jcalz in the comments):

When a type inference is made from several expressions, the types of those expressions are used to calculate a “best common type”. [...] The best common type algorithm considers each candidate type, and picks the type that is compatible with all the other candidates.

So if you use a non-empty object, no best common type will be found in the list, and since

When no best common type is found, the resulting inference is the union array type

You get a union of all item types as the result.

However, from the moment you include a single empty object in your list and you don't have any null or undefined items, Object becomes the best common type and the output will be an empty object.

Upvotes: 1

Cerberus
Cerberus

Reputation: 10218

It seems that both string and number are assignable to {}: playground (in fact, every type would be assignable to {}, except null, void and undefined). So TypeScript unifies three types to the widest one. When you had the first case, there wasn't one type to use among the three ones presented, so you've had a union.

Upvotes: 2

Related Questions