0

While working on a library, I discovered what to me looks like a bug when using Generics:

type R<A> = A extends Bottom ? A : A
type Bottom = { test: number }

const f = <A extends Bottom>(a: A) => {
    useIt(a) // type error here
}

const useIt = <A extends Bottom>(a: R<A>) => console.log(a)

As you can also see in the Playground example, for some unclear reason a cannot be used as R<A>, even though this type is equivalent to A.

The type error is:

Argument of type 'A' is not assignable to parameter of type 'R<A>'.
  Type 'Bottom' is not assignable to type 'R<A>'.

Using a concrete type instead of a generic will work as expected, eg:

type X = {test: 1}
const x: R<X> = {test: 1} // all good
const noX: R<X> = {test: 2} // error

Having a better restriction type will also work as expected for concrete types:

type R<A> = A extends Bottom ? A : never
const x: R<X> = {test: 1} // all good
const error: R<{}> = {} // type error as expected given that {} doesn't extend Bottom

So, is there any way to make it work with Generics?

2 Answers 2

1

This is more of a design limitation than a bug; unresolved conditional types (ones which depend on a yet-to-be-specified generic type parameter) are more or less deferred completely by the compiler, and almost nothing is seen as assignable to them.


There's an open issue, microsoft/TypeScript#23132, that suggests using generic constraints to determine assignability to unresolved conditional types; I think if this suggestion were implemented your example code would work (because A extends Bottom would be seen as true)... so you might want to go that issue and give it a 👍 and possibly explain your use case if you think it's more compelling than what's there.

There's also microsoft/TypeScript#33912, which proposes using control flow analysis to determine assignability to unresolved conditional types, which might also help if it were to be implemented.


Right now I think the only way to "make it work" is either to use type assertions, as in:

useIt(a as R<A>)

or to express your type so that it is no longer an unresolved conditional type, if possible; in your example code, R<A> is unconditionally A, so

// type R<A> = A extends Bottom ? A : A
type R<A> = A

would solve it.

Actually I see you changed R<A> in another part of your code to be essentially Extract<A, Bottom>. In some instances, Extract<T, U> can be replaced by the intersection T & U without ill effects; you might try that instead:

// type R<A> = A extends Bottom ? A : never
type R<A> = A & Bottom

That might also work.


Okay, hope that helps; good luck!

Link to code

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you a lot for all the info! My actual use case is a lot more complex. I'm trying to implement some flexible constraint for a UI library. Have a look at : github.com/microsoft/TypeScript/issues/…
In the end I solved the problem by adding the restriction to the other function too, see my answer for more details
0

After a lot of tinkering, I solved this problem by explicitly adding the restriction:

const f = <A extends Bottom>(a: R<A>) => {
    useIt(a) // works
}

const useIt = <A extends Bottom>(a: R<A>) => console.log(a)

Please note that now f argument has the same constraint of useIt, which will make the compiler happy. With hindsight, this actually makes sense, so that we are 100% sure the type is usable for useIt too :)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.