Home > database >  All possible types are narrowed but compiler still complains of missing return
All possible types are narrowed but compiler still complains of missing return

Time:11-29

I am seeing this error:

Function lacks ending return statement and return type does not include 'undefined'.

With this TypeScript code:

function decodeData(
  data: string | number[] | ArrayBuffer | Uint8Array,
): string {
  const td = new TextDecoder();
  if (typeof (data) === "string") {
    return data;
  } else if (data instanceof Array) {
    return td.decode(new Uint8Array(data));
  } else if (data instanceof ArrayBuffer) {
    return td.decode(data);
  } else if (data instanceof Uint8Array) {
    return td.decode(data);
  } 
}

My understanding is that the compiler will see all my guards cover all potential types of data. If I add an "else throw" it's happy.

What have I misunderstood?

CodePudding user response:

This is unfortunately a missing feature of TypeScript, requested at microsoft/TypeScript#21985.

Even though the compiler is able to narrow the type of data to the impossible never type at the bottom of the function, it doesn't seem to understand that this effectively makes the bottom of the function unreachable and therefore all reachable code paths do indeed return a value. That is, the compiler has no notion of an "exhaustive" set of if/else statements. Until and unless microsoft/TypeScript#21985 is implemented, then, you'll have to work around the problem.


The compiler does have a notion of an exhaustive switch statement, so one workaround for situations like this is to refactor to use switch instead. There's no obvious way to do that for decodeData(), so we will skip that.

Another approach is to introduce an assertion function assertNever(), which only accepts inputs that have been narrowed to never, and which itself unequivocally throws so that the compiler knows that subsequent code is unreachable:

function assertNever(x: never): never {
    throw new Error("Unexpected Value: "   x);
}

Then, at the end of decodeData(), you call assertNever(data):

function decodeData(
    data: string | number[] | ArrayBuffer | Uint8Array,
): string {
    const td = new TextDecoder();
    if (typeof (data) === "string") {
        return data;
    } else if (data instanceof Array) {
        return td.decode(new Uint8Array(data));
    } else if (data instanceof ArrayBuffer) {
        return td.decode(data);
    } else if (data instanceof Uint8Array) {
        return td.decode(data);
    }
    assertNever(data);
}

That suppresses the "not all paths return a value" error, since assertNever() definitely throws, and it also checks for exhaustiveness, since otherwise assertNever(data) wouldn't compile successfully:

function badDecodeData(
    data: string | number[] | ArrayBuffer | Uint8Array,
): string {
    const td = new TextDecoder();
    if (typeof (data) === "string") {
        return data;
    } else if (data instanceof Array) {
        return td.decode(new Uint8Array(data));
    //} else if (data instanceof ArrayBuffer) {
    //    return td.decode(data);
    } else if (data instanceof Uint8Array) {
        return td.decode(data);
    }
    assertNever(data); // error! 
    // -------> ~~~~
    // Argument of type 'ArrayBuffer' is not assignable to parameter of type 'never'
}

Here I've commented out one of the cases, and sure enough, assertNever(data) complains that ArrayBuffer (the missing case) is not a valid never.


Playground link to code

CodePudding user response:

function decodeData(
  data: string | number[] | ArrayBuffer | Uint8Array,
): string {
  const td = new TextDecoder();
  if (typeof (data) === "string") {
    return data;
  } else if (data instanceof Array<number>) {
    return td.decode(new Uint8Array(data));
  } else if (data instanceof ArrayBuffer) {
    return td.decode(data);
  } else if (data instanceof Uint8Array) {
    return td.decode(data);
  }

  return '';
}
  • Related