Issue
I have following simplified code:
export type RT<T> = T extends Array<unknown> ? {v: T} : T;
function test<T extends Array<unknown>>(value: RT<T>): value is {v: T} {
return "v" in value;
}
It gives me the error:
A type predicate's type must be assignable to its parameter's type. Type '{ v: T; }' is not assignable to type 'RT'
Isn't the type { V: T; } assignable to type RT. Even though it is conditional, it must be assignable.
I must preserve the generic.
Maybe I should use different approach, the problem is I need to check and cast a "tree" type into "leaves" of different types.
Solution
When you have a conditional type that depends on a generic type parameter, like RT<T>
, the compiler tends to defer evaluation of it until the generic type argument has been specified. That means the compiler really has no idea what might or might not be assignable to RT<T>
before it knows what T
is.
This is still true even when T
is constrained to a type that fits entirely on the true branch or entirely on the false branch of the conditional type. You might hope or expect that T
being constrained to Array<unknown>
would allow the compiler to evaluate T extends Array<unknown> ? { v: T } : T
to { v: T }
even before T
is specified, but this does not happen.
Yes, the generic constraint syntax <T extends U>
is the same as the conditional type check syntax T extends U ? X : Y
, but this unfortunately doesn't mean the compiler can use the former to evaluate the latter early.
There are various issues in GitHub about this, such as microsoft/TypeScript#31096 and microsoft/TypeScript#56045. These are generally closed as design limitations. For the foreseeable future, this is just how the language is.
So the compiler doesn't know that {v: T}
will be assignable to RT<T>
inside the call signature for test()
, and thus it won't let you return value is RT<T>
, since type predicates must represent narrowings, not arbitrary type mutations.
If you want to proceed with a function like this, you'll need to rewrite the call signature. Generally speaking if you know that type U
is assignable to type V
but the compiler doesn't, you can use the intersection U & V
in place of U
, since it knows that U & V
is assignable to V
no matter what U
is. And if you are correct about the assignability, then U & V
will be more or less equivalent to U
. (You could also use Extract<U, V>
with the Extract
utility type). That means we can write
function test<T extends Array<unknown>>(value: RT<T>): value is RT<T> & { v: T } {
return "v" in value;
}
and it will compile. This should be enough for you to proceed.
Aside: presumably your actual use case motivates the type guard function. But taken at face value it doesn't seem useful. If we try to evaluate RT<T>
more aggressively than the compiler does, and say that it must be {v: T}
because T
is constrained to Array<unknown>
, then this results in:
function test<T extends Array<unknown>>(value: { v: T }): value is { v: T } {
return "v" in value;
}
which means you will be calling test(value)
as a way to check that value
is of the type you already know it is. That is, it's completely impossible for this function to return false
and there's no obvious reason why you'd want or need to check this. So while this example is sufficient for demonstrating how to alter a type predicate to be accepted, it doesn't seem to do so for any apparent purpose. That's out of scope for the question as asked, though, so I won't digress any further.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.