In the following snippet
interface C1 { kind: 'c1' }
interface C2 { kind: 'c2' }
interface C3<T> { kind: 'c3'; value: T }
function isC3<T, CX extends ([C3<T>] extends [CX] ? { kind: string; } : never)>(
c: CX
): c is C3<T> /* <------- PROBLEM !!! */ {
return c.kind === 'c3';
}
// valid use
type C = C1 | C2 | C3<number>;
isC3<number, C>({ kind: 'c3', value: 1 }); // true
isC3<number, C>({ kind: 'c1' }); // false
// invalid use
type D = C1 | C2;
declare var d: D;
isC3<number, D>(d); // D doesn't satisfy constraint never (as expected)
I am trying to make the isC3
typeguard function in such a way that a call to it only compiles if the type of the given type argument has C3
case in it, otherwise I would like to get a compilation error (please look at the examples)
So everything works, except that I get an error at the typeguard return type:
A type predicate's type must be assignable to its parameter's type.
Type 'C3<T>' is not assignable to type 'CX'.
'C3<T>' is assignable to the constraint of type 'CX', but 'CX' could be instantiated with a different subtype of constraint '{ kind: string; }'.ts(2677)
Is there a way to get what I am looking for?
If you have a type T
that you know is assignable to U
but the compiler does not know this, and you need to use T
in a place the compiler expects something assignable to U
, you can either use the Extract<T, U>
utility type or possibly Extract<U, T>
, or you can use the intersection T & U
, depending on your use case:
declare function isC3<T, CX extends ([C3<T>] extends [CX] ? { kind: string; } : never)>(
c: CX
): c is Extract<CX, C3<T>> // okay
or
function isC3<T, CX extends ([C3<T>] extends [CX] ? { kind: string; } : never)>(
c: CX
): c is CX & C3<T> {
return c.kind === 'c3';
}
Both of these end up resulting in a type which is effectively C3<T>
. Extract<T, U>
filters a union in T
to just those members assignable to U
(so if you expect CX
to be a union, then Extract<CX, C3<T>>
is reasonable), while T & U
is more agnostic about unions.