Typescript: TS fails to understand guard for Array.prototype.pop()

Created on 14 Mar 2019  路  7Comments  路  Source: microsoft/TypeScript

TypeScript Version: 3.4.0-dev.201xxxxx

Search Terms:

  • array.pop
  • Object is possibly 'undefined'

Code

const arr: string[] = [];

if (Math.random() > 0.5) { // <-- simulate runtime unknown array length
    arr.push('hi');
}

if (arr.length > 0) {
    arr.pop().toLowerCase(); // <-- error `Object is possibly 'undefined'`
}

Expected behavior:
No error, because we tested that the array's length was greater than zero.

Actual behavior:
Error: Object is possibly 'undefined'

Playground Link:
_Note: turn on all strict checks in "options" (the link doesn't preserve these settings)_
http://www.typescriptlang.org/play/index.html#src=const%20arr%3A%20string%5B%5D%20%3D%20%5B%5D%3B%0D%0A%0D%0Aif%20(Math.random()%20%3E%200.5)%20%7B%0D%0A%20%20%20%20arr.push('hi')%3B%0D%0A%7D%0D%0A%0D%0Aif%20(arr.length%20%3E%200)%20%7B%0D%0A%20%20%20%20%2F%2F%20NOTE%3A%20turn%20on%20all%20strict%20checks!%0D%0A%20%20%20%20arr.pop().toLowerCase()%3B%0D%0A%7D%0D%0A

Related Issues:
https://github.com/Microsoft/TypeScript/issues/29642 -- maybe? 馃

Design Limitation

Most helpful comment

A simple way to let Typescript know that it can assume 'not undefined' is to use an explicit cast:

if (arr.length > 0) {
    (arr.pop() as string).toLowerCase();
}

All 7 comments

I think is essentially the same thing as #9619, or rather, it suffers from the same limitations.

This isn't something we're capable of tracking

you just need an interface that represents a non-empty array and a type guard function:

const arr: string[] = [];
interface NonEmptyArray<T> extends Array<T> {
    // would need to implement all relevant functions.
    pop: () => T;
}

function isNonEmpty<T>(arr: Array<T>): arr is NonEmptyArray<T> {
    return arr.length > 0;
}
if (isNonEmpty(arr)) {
    // here we are sure arr.pop() will return a value!
    arr.pop().toLowerCase();
}

Edit: never mind, in the general case there really isn't any way to ensure pop is valid since if you pop more than once this would still act like each one was guaranteed not to return undefined, so this doesn't get caught by typescript:

if (isNonEmpty(arr)) {
    arr.clear();
    // typescript still under assertion that arr isn't empty!
    arr.pop().toLowerCase();
}

@tadhgmister This would require something like #29346, #27568, #25679, #8655, #10421. A way to indicate that a function will mutate and change the type of an argument.

A simple way to let Typescript know that it can assume 'not undefined' is to use an explicit cast:

if (arr.length > 0) {
    (arr.pop() as string).toLowerCase();
}

@basvanmeurs A not null assertion:

if (arr.length > 0) {
    arr.pop()!.toLowerCase();
}

Is this relevant?

class Stack<T> {
  private data: T[] = [];
  constructor() {}
  push(item: T): void {
    this.data.push(item);
  }
  pop(): T | 0 {
    if (this.data.length > 0) return this.data.pop(); // Error : Type 'undefined' is not assignable to type '0 | T'.
    else return 0;
  }
}

EDIT: below code has no problem. Weird i think.

class Stack<T> {
  private data: T[] = [];
  constructor() {}
  push(item: T): void {
    this.data.push(item);
  }
  pop(): T  {
     return this.data[this.data.length]; // no error but it could actually return 'undefined'
  }
}
Was this page helpful?
0 / 5 - 0 ratings