The problem at hand mirrors the issues highlighted in microsoft/TypeScript#23132 and microsoft/TypeScript#45281. When dealing with conditional types, TypeScript fails to carry over the constraints from the checked type to the output type as expected.
One might assume that for
type Foo<T extends string> = [T] extends [infer Y] ? Y : never
TypeScript would recognize that T
is restricted to string
, thereby limiting the inferred Y
to string
so that Foo<T>
would effectively be constrained to string | never
, which equals just string
. However, this inference does not occur. There's no evident constraint on Y
, leading it to be implicitly restricted to unknown
.
Consequently, when using Foo<T>
in scenarios where T
is generic, TypeScript treats it potentially as unknown
, resulting in errors in situations requiring something more precise than unknown
:
type Test = { [T in "foo" as Foo<T>]: undefined } // error!
// ~~~~~~
// Type 'Foo<T>' is not assignable to type 'string | number | symbol'.
// Type 'unknown' is not assignable to type 'string | number | symbol'.
Note that this discrepancy doesn't arise when you specify Foo<T>
with a specific type like Foo<"foo">
or Foo<string>
. This straightforward evaluation occurs because TypeScript can directly ascertain these types as "foo"
and string
, respectively. In such cases, there's no issue of "constraint" since those types are known quantities. TypeScript doesn't falter with { [K in "foo"]: 0 }
, thus also handling
{ [K in Foo<"foo">]: 0 }
; both being identical types.
Therefore, TypeScript fails to extend constraints through conditional types in similar circumstances. To address this limitation, one must navigate workarounds. The simplest solution involves utilizing extends
constraints within infer
clauses. Instead of solely declaring infer Y
, write infer Y extends string
:
type Foo<T extends string> = [T] extends [infer Y extends string] ? Y : never
// ^^^^^^^^^^^^^^
type Test = { [T in "foo" as Foo<T>]: undefined } // now valid
This conveys to TypeScript that the conditional type should only hold true if the inferred Y
behaves as a subtype of string
. This adjustment doesn't alter the functionality of Foo<T>
for specific T
, since we already understood Y
to be a subclass of string
. Therefore, both Foo<string>
and
Foo<"foo"></code persist unchanged. Yet, now TypeScript imposes a requirement of <code>string
on
Y
, consequently constraining
Foo<T>
to
string
and enabling successful compilation of
Test
as shown above.
Playground link to code