Home > Mobile >  Typescript Generic List Type Narrowing
Typescript Generic List Type Narrowing

Time:07-20

I'm struggling to understand why I'm getting an error with this code.

type Foo = "foo" | "FOO";
type Bar = "bar" | "Bar";

type FooBar = Foo | Bar;

function isFoo(f: FooBar): f is Foo {
  return f === "foo";
}

function isBar(b: FooBar): b is Bar {
  return b === "bar";
}

function convertToUpperCaseForm<T extends FooBar>(i: T): T {
  if (isFoo(i)) return "FOO";
  else return "bar";
}

function doSomething<T extends FooBar>(items: T[]): T[] {
  if (items.length === 0) return [];
  else if (items.length === 1 && isFoo(items[0])) return ["FOO"];
  else return items.map(convertToUpperCaseForm)
  /**
    * Return a list of the same type. If I put a list of Foo
    * in I want a list of Foo out.
    */
}

Basically I'm saying I have a list that takes a generic which is a type union. If the first item in that list is one of the types, return another object of that same type. However, this is giving me the error:

Type '"foo"' is not assignable to type 'T'.
'"foo"' is assignable to the constraint of type 'T', but 'T' could
be instantiated with a different subtype of constraint 'FooBar'.

Now, the reason I think this should work is that with the (isFoo(items[0])) check we have confirmed that a list of type T[]'s first elemnt is of type Foo, ergo, T must be at least of type Foo, so returning "foo" is a legal return of type T.

Is this a bug in Typescript? Is there a way I can work around this without type assertions? I believe it's typesafe, but I know with these kinds of errors there's usually a good explanation.

EDIT1: This isn't a perfect example of what I have in my codebase. A few people have pointed out things that aren't issues in my codebase. Just know that this is the error I'm getting, I do need the generic, and I do need to return an object of the same type I put in.

Edit2: Updated code snippet to reflect my codebase. I'm getting the same error in the convertToUpperCaseForm function.

Type '"FOO"' is not assignable to type 'T'.
'"FOO"' is assignable to the constraint of type 'T', but 'T' could
be instantiated with a different subtype of constraint 'FooBar'.

I find this really odd because I was under the impression that this was a result of the Array index. if convertToUpperCaseForm receives an object of type Foo, it should allow me to return an object of type Foo.

CodePudding user response:

why you need the genric if you already know the type?

type Foo = "foo";
type Bar = "bar";

declare type FooBar = Foo | Bar;

function isFoo(f: FooBar): f is Foo {
  return f === "foo";
}

function isBar(b: FooBar): b is Bar {
  return b === "bar";
}

function doSomething(items: FooBar[]): FooBar {
  if (isFoo(items[0])) return "foo";
  if (isBar(items[0])) return "bar";
  return items[0];
}

console.log(doSomething(["foo", "bar"]));

// bar

CodePudding user response:

There is a flaw in your logic (the signature is wrong):

function convertToUpperCaseForm<T extends FooBar>(i: T): T {
  if (isFoo(i)) return "FOO";
  else return "bar";
}

Let's say i is of type "foo", meaning that T is "foo". The function is supposed to return something of the same type, T, so "foo" but you returned "FOO" which is different from "foo".

const i = "foo" // i: "foo"
convertToUpperCaseForm(i) // (i: "foo"): "foo"

function convertToUpperCaseForm<T extends FooBar>(i: "foo"): "FOO" {

So you cannot say it takes T and returns T because as you can see, there is a case where it doesn't work.

  • Related