简体   繁体   中英

Typescript custom restriction with Generics

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?

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

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:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM