In my large compiler project, I have isolated the error code to this:
type UserType = {
email: string
}
type ScopeType<S, P extends unknown = unknown> = {
data: S
parent?: P extends ScopeType<infer T, infer Q>
? ScopeType<T, Q>
: never
}
type PossibleScopeType<ST> = ST extends ScopeType<
infer S,
infer P
>
? ST | (P extends ScopeType<infer A, infer B> ? (P | PossibleScopeType<P>) : never)
: never
type ModuleType = {
path: string
}
const scope: ScopeType<ModuleType> = {
data: {
path: './foo.x',
},
}
const scope2: ScopeType<UserType, ScopeType<ModuleType>> = {
data: {
email: '[email protected]',
},
parent: scope,
}
let scope3: PossibleScopeType<typeof scope2> = scope
scope3 = scope
let scope4: PossibleScopeType<ScopeType<UserType, ScopeType<ModuleType>>> = scope
scope3 = scope
export type ParentScopeType<S> =
// | S
S extends ScopeType<infer X, infer Y> ? S : S
function test<T, S extends ScopeType<T>>(scope: S): void {
let source: ParentScopeType<S> | undefined = scope
console.log(source)
}
test<UserType, ScopeType<UserType>>(scope2)
In the function test
at the bottom, this line throws an error at source
:
let source: ParentScopeType<S> | undefined = scope
It says:
Type 'S' is not assignable to type 'ParentScopeType<S> | undefined'.
Type 'ScopeType<T, unknown>' is not assignable to type 'ParentScopeType<S>'.(2322)
I have struggled with this for a few hours. Literally in this case I am just debugging, and in ParentScopeType
I simply return S
(ScopeType
) in both conditional branches! If I comment out the conditional branching and just put the following, it compiles:
type ParentScopeType<S> = S
That is the same as doing:
function test<T, S extends ScopeType<T>>(scope: S): void {
let source: S | undefined = scope
console.log(source)
}
I don't see why the conditional branch changes the logic at all, because it is returning S
in both branches... Something is fishy with this, my understanding of how conditional types work must be off. Can you show me how I can keep the function test
as is, and get it to compile? The ParentScopeType
should be a type of the current scope and all its parents, like Scope1 | Scope1Parent | Scope1ParentParent | ...
.
Here is another slight variation, same error.
An even more primitive example is:
type ParentType<S> = S extends object ? string : number
function test2<T>(scope: T): void {
let source: ParentType<T> | undefined = 'hello'
console.log(source)
}
I get the error:
Type 'string' is not assignable to type 'ParentType<T>'
Why is it not working like I'd expect? Shouldn't source: string | number
in the last example?
It compiles like this though:
type ParentType<S> = S extends string ? string : unknown
function test2<T>(scope: T): void {
let source: ParentType<T> | undefined = 'hello'
console.log(source)
}
I don't understand what's going on.
CodePudding user response:
As I understand it, it has to do with the fact that conditional types become distributive when handed a union type (see the handbook here).
This fails:
type Generic<T> = T extends string ? T : T;
function test<T>(t: T): void {
const x: Generic<T> = t; // Type 'T' is not assignable to type 'Generic<T>'.
}
To counter the distributive behavior, you can wrap the type parameter being checked in a tuple, so this works:
type Generic<T> = [T] extends [string] ? T : T;
function test<T>(t: T): void {
const x: Generic<T> = t; // OK
}
For your example, if you change your ParentScopeType
as below, you'll get rid of the compiler error:
export type ParentScopeType<S> = [S] extends [ScopeType<unknown>] ? S : S;