I'm working with a data structure that exists in two variants: a complete version for the output and an incomplete version where some properties are nullable during intermediate processing. Most functions expect the complete version of the type, some can work with both. Since it's a complex tree-like data structure with a lot of discriminated unions, instead of defining the type for the incomplete variant as a union, I wanted to use a generic parameter, the basic idea being
interface Data<Mode = 'DONE'> {
someProperty: Mode extends 'BUILD' ? string | null : string;
}
type FullData = Data<'DONE'>; // or just `Data`, using the default
type TempData = Data<'BUILD'>;
Now some functions will want to check whether the data is actually complete, and can then call another function that expects the complete data:
function example(doneVal: Data<'DONE'>) {}
function caller(buildVal: Data<'BUILD'>) {
if (buildVal.someProperty != null) {
example(buildVal);
}
}
However, this produces the error message
Argument of type 'Data<"BUILD">' is not assignable to parameter of type 'Data<"DONE">'.
Type '"BUILD"' is not assignable to type '"DONE"'.
(ts2345)
Ok, I get it, TypeScript cannot infer from the someProperty != null
check that buildVal
is of the correct type, but even when I use a type assertion
example(buildVal as Data<'DONE'>);
Conversion of type 'Data<"BUILD">' to type 'Data<"DONE">' may be a mistake because neither type sufficiently overlaps with the other.
Type '"BUILD"' is not comparable to type '"DONE"'
What gives? It seems TypeScript is not actually comparing the types structurally, but rather by name?!
If I were to spell out the types in both variants instead of using a single generic type, it works just fine.
I can work around this by using a type guard (which also means I don't have to explicitly assert the type in the call), but I'd still like to understand what is happening. So far, my experimentation shows that the error message changes when I use 'BUILD' extends Mode
instead of Mode extends 'BUILD'
in the type condition (I never know which one to use), and that the default parameter type does not matter (Data<Mode>
or Data<Mode extends 'DONE' | 'BUILD'>
).
Edit: after some further experimentation, I found that using type TempData = Data<'BUILD'> | Data<'DONE'>
or Data<'BUILD' | 'DONE'>
lets me do the cast, but I don't like the verbosity of that. And I'd still like to understand why my original attempt doesn't work :-)