I have the following type definition in Typescript:
type MU = ['I' | 'U' | 'M', MU] | '.'
And I'm trying to write the helper:
type EndsWith<T extends MU, M extends MU> = T extends M ? any : T extends [infer _, infer Q] ? EndsWith<Q, M> : never
Which would check if a specific type T
ends with a given M
, i.e.:
const a: EndsWith<['U', ['I', '.']], ['I', '.']> // types as any
const b: EndsWith<['U', '.'], ['I', '.']> // types as never
However, EndsWith
is not an aliased type in the way I am specifying it. Looking for a workaround :)
Circular conditional types are not currently supported. See microsoft/TypeScript#26980 for an open issue suggesting that this constraint be lifted. For now, they are forbidden.
Note that it is quite possible to trick the compiler into evaluating circular conditional types, but if you do that and it behaves badly, it's your problem and not TypeScript's.
Here's one such trick. It is not supported:
type EndsWith<T extends MU, M extends MU> =
T extends M ? any :
{
base: never,
step: EndsWith<T extends any[] ? T[1] : never, M>
}[T extends '.' ? "base" : "step"];
What we've done is to turn your obviously circular type into a recursive, tree-like object that we immediately index down into with a deferred conditional type. That's still circular, but the compiler doesn't notice it.
It "works":
declare const a: EndsWith<['U', ['I', '.']], ['I', '.']> // any
declare const b: EndsWith<['U', '.'], ['I', '.']> // never
but is fragile. If you write this:
declare const c: EndsWith<MU, ['I', MU]> // oops
you are asking the compiler to evaluate EndsWith<MU, ['I', MU]>
by first checking if MU extends ['I', MU]
(it doesn't), and then recursing down into... EndsWith<MU, ['I', MU]>
again. Uh oh. There will be a compiler problem somewhere; most likely you'll get a "this type is too deeply instantiated error. If you're really clever you can probably write some code using the above EndsWith
that will actually cause the compiler to hang instead of spitting out an error.
I get the sense that you're just writing EndsWith
for academic purposes (since lists implemented as cons-like pairs is not idiomatic JS or TS) so maybe the fact that it's unsupported isn't a problem. If I'm wrong and you want this for production code somewhere, please don't. I don't want to be an accessory to crimes against the compiler.
Hope that helps; good luck! Playground link to code