NG_
NG_

Reputation: 265

Generics and nullish types

Say I'm interested in a generic function that will give a compile time error if called with arguments of different types.

Initially, I've tried something like that:

type genericFunc = {
    <T>(a:T, b:T): boolean
}

let isEq: genericFunc = (a,b) => { return a === b }

When I try to call it like so:

isEq(1,"str")

I get a compile time error, as expected. However, when I try to:

isEq(1, null)

or:

isEq(1, undefined)

It all compiles without complaining. Per vscode, the type of isEq(1, null) is inferred to as:

let isEq: <number | null>(a: number | null, b: number | null) => boolean

Setting "strictNullChecks": true in my tsconfig.json does not change that.

To summarize, I'm interested in the following questions:

  1. Why are the types inferred this way?
  2. Should I define the type genericFunc differently?
  3. Is there a way to enforce a stricter type inference when generics and nullish types are involved?

Many Thanks!

Upvotes: 0

Views: 185

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187054

T can be a union, so it could always just be a union of the two argument to satisfy the function's type.

Though I'm not really sure why isEq(1, "str") fails since it seems like number | string seems like it should be the inferred type there, but helping out the compiler with a isEq<number | string>(1, "str") does seem to allow it there. I wish I could tell you why that happens.


An improvement would be to have two generic parameters, one for each argument, and ensure that one extends the other.

<T, U extends T>(a:T, b:U): boolean

Now U must be the same type (or a subtype) of T in order to be allowed.

// works
isEq('asd', 'qwe')

// All type errors:
isEq(1, "str")
isEq(1, null)
isEq(1, undefined)

Playground


All that said, the "same" type is a trickier thing than you might expect.

This solution would allow:

isEq({a: 234}, {a: 123, b: 456})

Because the second argument is a subtype of the first. Meaning it would be assignable to that type. Which raises the question:

Are these the same type?

const a = { a: 123 }
const b = { a: 456, b: 789 }

How about these?

type Test = { a: number, b?: number }
const a: Test = { a: 123 }
const b: Test = { a: 456, b: 789 }

Or these?

class A { public num: number }
class B extends A { public str: string }

Or?

const a: string | number = 123
const b: string | number = 'asd'

Because all these would be considered equal by this approach, because b in each case is assignable to the the type of a. Maybe that's fine, maybe it's not.

How to compare these types really depends on how you intend to use this.

Upvotes: 1

Related Questions