javascripttypescripttypeguardstype-narrowingtypescript4.0

Why doesn't a type narrowing check of a class property not work when assigned to a variable (aliased conditions)?


How can I narrow the type of property of a class in an aliased conditional expressions?

Short: In a class method I want to do a type narrowing check such as this._end === null && this._head === null, but I want to first assign the result of this check to a variable before using it in a type narrowing if clause. It's not working as expected.

Not short: Let's say we have a Queue class:

class Queue {
  private _length: number = 0;
  private _head: Node | null = null;
  private _end: Node | null = null;

  public enqueue(node: Node): boolean {
    this._length += 1;

    const isQueueEmpty = this._end === null;
    if (isQueueEmpty) {
      this._head = node;
      this._end = node;
      return true;
    }

    this._end.setLink(node); // TypeScript doesn't understand that this._end passed the null check 
                             // in the if statement and writes that this._end can be null. 
    this._end = node;
    return true;
  }
}

If you write a check in the enqueue function directly in the if condition, then TypeScript understands that this._end is not null:

public enqueue(node: Node): boolean {
  this._length += 1;

  if (this._end === null) {
    this._head = node;
    this._end = node;
    return true;
  }

  this._end.setLink(node); // Now TypeScript understand that this._end passed the null check
  this._end = node;
  return true;
}

But the code turns out to be less descriptive.

Moreover, in general, I need to say that both properties of the class passed the null check. Specifically: this._head and this._end.

I wrote the following method:

type QueuePointerKeys = '_head' | '_end';

private _isQueueEmpty(): this is this & { [K in QueuePointerKeys]: null } {
  return this._end === null;
}

And it doesn't work. If you put it in the enqueue method, then:

public enqueue(node: Node): boolean {
  this._length += 1;

  if (this._isQueueEmpty()) {
    this._head = node;     // TypeScript writes that a variable of type null cannot be assigned the type Node
    this._end = node;      // TypeScript writes that a variable of type null cannot be assigned the type Node
    return true;
  }

  this._end.setLink(node); // TypeScript doesn't understand that this._end passed the null check
                           // in the if statement and writes that this._end can be null.
  this._end = node;
  return true;
}

How do I use a class method such as _isQueueEmpty for type narrowing? That works similarly to this._end === null && this._head === null used directly as the condition of the if statement?


Solution

  • Control Flow Analysis of Aliased Conditions and Discriminants was only recently added, and it has limitations, including:

    Narrowing through indirect references occurs only when the conditional expression or discriminant property access is declared in a const variable declaration with no type annotation, and the reference being narrowed is a const variable, a readonly property, or a parameter for which there are no assignments in the function body.

    Since this._end is not a readonly property, it's not going to work for you. I confirmed that this is your problem by assigning this._end to a local const variable, and then using it for the condition as well as the setLink call:

    interface Node {
      setLink(node: Node): void
    }
    
    class Queue {
      private _length: number = 0;
      private _head: Node | null = null;
      private _end: Node | null = null;
    
      public enqueue(node: Node): boolean {
        this._length += 1;
    
        const end = this._end
        const isQueueEmpty = end === null;
    
        if (isQueueEmpty) {
          this._head = node;
          this._end = node;
          return true;
        }
    
        end.setLink(node);
        this._end = node;
        return true;
      }
    
    }
    

    In fact, there is an open issue addressing your specific case: Control flow analysis of aliased conditions is not able to narrow object properties #46412, as well as Allow non-readonly properties to be used in aliased conditional expressions #44972.

    Let's hope they can find solutions for these soonish 🤞🏾