Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: mixed behaviour for function with multiple parameters of same generic type

Given a function which receives two parameters of the same generic type:

declare const fn: <T>(t1: T, t2: T) => any;

I am trying to reason about the different behaviour TypeScript has when this function is invoked.

When the 2 parameters are both different primitives:

fn(1, 'foo'); // Error due to different types

When the 2 parameters are both different objects:

fn({ foo: 1 }, { bar: 1 }) // No error and return type is union of different types

How come these two usages don't have the same behaviour? I would expect them both to behave the same. Either:

  • Error due to different types
  • No error and return type is union of different types

Secondly, TypeScript's behaviour is different again if one of the parameters passed in is a variable (rather than an inline object literal):

fn({ foo: 1 }, { bar: 1 }) // No error and return type is union of different types

const obj = { foo: 1 };
fn(obj, { bar: 1 }) // Error due to different types

Again, how come these two usages don't have the same behaviour? I would expect both of these cases to behave the same.

like image 914
Oliver Joseph Ash Avatar asked Jun 14 '19 18:06

Oliver Joseph Ash


1 Answers

The compelling use case for this behavior comes from examples like this, where you have some candidate set of types, none is a supertype of the others, but the intended inferred type parameter ({ a?: number, b?: number, c?: number }) is pretty obvious:

declare const fn: <T>(t1: T, t2: T, t3: T) => any;

fn({ a: 0, b: 1 }, { b: 2, c: 3 }, { a: 4, c: 5 });

Why does this not happen when the arguments aren't object literals?

When you have some binding foo with some type T, TypeScript can't* know that the object pointed to by foo has exactly the type T - it might have more properties that weren't declared in T, but ended up bound to foo via a subtype relation. This is very common in practice.

For this reason, in the example in the OP, inferring the type { foo?: number, bar?: number } would be unsound because obj might have pointed to an object with a bar property of type string.

* You could add more special cases around consts which were initialized with an object literal that wasn't type-asserted to something else, but that would just make things even more inconsistent

like image 134
Ryan Cavanaugh Avatar answered Sep 18 '22 17:09

Ryan Cavanaugh