I'm trying to narrow down a complex type using a type guard. I would like the false branch of the type guard to understand the complement to the narrowed down type.
interface Model { side: 'left' | 'right'; }
interface LeftModel { side: 'left'; }
interface RightModel { side: 'right'; }
type Either = LeftModel | RightModel;
function isLeft(value: Either): value is LeftModel { // else is RightModel
return value.side === 'left';
}
This does not seem possible, at least not the way I'm going about it. Typescript is happy to infer that an Either
can be a model, but it doesn't accept that a Model
can be an Either
. This is giving an error:
declare const model: Model;
isLeft(model) // ts(2345)
Is this problem fundamentally unsolvable?
If not, how do I get the false branch to narrow down to the complement?
see full example in this Typescript Playground
EDIT
It appears in this naive example that Model
and Either
are equivalent, but this probably cannot be generalised. I could make some progress by combining two type guards, in an attempt to inform the type system that Model
is in fact a valid Either
(see this new Playground). This however leaves me with an unwanted branch (see line 22) so isn't fully satisfactory.
Is there a way to let the type system know that Either
and Model
are strictly equivalent?
I do not particularly need to use either type guards or union types, this was my first attempt at solving the problem but created problems of its own. Union types would only be viable if we can guarantee that the union of a narrowed type and its relative complement are indeed equivalent to the type narrowed down. This relies on the type system having an understanding of complement, which might not be the case (yet). See this typescript complement search and the handbook on utility types
Someone suggesting using fp-ts and/or monocle-ts to solve this but some of these functional programming concepts are still going over my head. If someone knows how to apply them here though that would be nice. Either sounds like it could help here...
A type guard is a TypeScript technique used to get information about the type of a variable, usually within a conditional block. Type guards are regular functions that return a boolean, taking a type and telling TypeScript if it can be narrowed down to something more specific.
What does ?: mean in TypeScript? Using a question mark followed by a colon ( ?: ) means a property is optional. That said, a property can either have a value based on the type defined or its value can be undefined .
TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing.
Type narrowing is just what it sounds like—narrowing down a general type into something more precise. If you've ever dealt with union types, e.g. string | number you've certainly encountered this.
The union operator |
does not perform a union on the types of the properties of the types specified in the union.
i.e.
type Either = LeftModel | RightModel === { side: 'left ' } | { side: 'right' }
!== { side: 'left' | 'right' } === Model
Either
is strictly a union of LeftModel
and RightModel
not a union of type side
and side
Excerpt of the error, I think this says it all,
Argument of type 'Model' is not assignable to parameter of type 'Either'. Type 'Model' is not assignable to type 'RightModel'. Types of property 'side' are incompatible. Type '"left" | "right"' is not assignable to type '"right"'. Type '"left"' is not assignable to type '"right"'.
You can actually do this with a type union and without a type guard:
interface Model { side: 'left' | 'right'; }
interface LeftModel extends Model { side: 'left'; }
interface RightModel extends Model { side: 'right'; }
function whatever(model: LeftModel | RightModel) {
model.side // left or right
if (model.side === 'left') {
model.side; // left - model is also LeftModel at this point
} else {
model.side; // right - model is a RightModel
}
}
Playground
The inheritance from Model
is actually optional, as it's really the type union that does the work here. But it helps to constrain any subclasses to 'right' or 'left' only.
Hopefully this is the type of thing you're trying to accomplish?
While the type guard isn't necessary, it can still be used. It can't figure out the complementary RightModel
type - but the caller can, by already having value
constrained to a union of LeftModel
and RightModel
.
interface Model { side: 'left' | 'right'; }
interface LeftModel extends Model { side: 'left'; }
interface RightModel extends Model { side: 'right'; }
function isLeft(value: Model): value is LeftModel {
return value.side === 'left';
}
function whatever(model: LeftModel | RightModel) { // This union makes the else branch work
model.side // left or right
if (isLeft(model)) {
model.side; // left - model is also LeftModel at this point
} else {
model.side; // right - model is a RightModel
}
}
With type guard
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With