Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type guard with complement in false branch

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...

like image 962
Renaud Avatar asked Jun 07 '19 16:06

Renaud


People also ask

What is type guarding?

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 is ?: In TypeScript?

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 .

What is type narrowing in TypeScript?

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.

What is type narrowing in angular?

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.


2 Answers

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"'.

like image 145
Avin Kavish Avatar answered Sep 24 '22 12:09

Avin Kavish


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

like image 38
UncleDave Avatar answered Sep 23 '22 12:09

UncleDave