I'm trying to make a utility function which will take either a tuple or an array and return whichever it took. I've constructed a generic type which understands whether it is passed a tuple or an array, but when I try to use the generic in my utility function, the generic doesn't detect the tuple:
type Foo = [number, number]
type MaybeFoo<T> = T extends Foo ? Foo : number[]
const bar = [1, 1, 1, 2]
const baz: Foo = [1, 1]
type WasntFoo = MaybeFoo<typeof bar> // WasntFoo is number[], which is what I want
type WasFoo = MaybeFoo<typeof baz> // WasFoo is [number, number], which is what I want
const utilityFunction = <T,>(arr: MaybeFoo<T>): MaybeFoo<T> => arr // in real life this does something to each el of arr but doesn't change the type
const r1 = utilityFunction(bar)
const r2 = utilityFunction(baz) // r2 is number[], when I want [number, number]
This is in TypeScript 4.8.4, playground here
CodePudding user response:
This is because the TypeScript compiler can not correctly infer T
in your function call. In this case the compiler defaults to unknown
and as unknown
does not extend Foo
the type will be evaluated as number[]
. If you add a typing to your function call it works as expected:
const r2 = utilityFunction<Foo>(baz)
CodePudding user response:
Taking into account your second example where Foo
is a different array type, I can only make it work without specifying the type argument explicitly by using a type assertion that seems safe to me (but I've been wrong many times about that, which is why I hate type assertions). First, a quick adjustment to MaybeFoo
's false branch to just result in the input type:
type MaybeFoo<T extends Foo | any[]> = T extends Foo ? Foo : T;
Then the function applies a constraint on T
, either T extends Foo | number[]
(in your original example) or T extends Foo | Movie[]
(in your second example); here's the number[]
version:
const fooFunction = <T extends Foo | number[],>(arr: T): MaybeFoo<T> => arr as MaybeFoo<T>;
Your test cases (plus a few):
const r1 = fooFunction(bar);
// ^? const r1: number[]
const r2 = fooFunction(baz); // r2 is number[], when I want [number, number]
// ^? const r2: Foo
const r3 = fooFunction([42]);
// ^? const r3: [number]
const r4 = fooFunction([]);
// ^? const r4: []
const rX = fooFunction(["x"]); // Error as desired, doesn't fit the constraint
// ^? (irrelevant, it's an error)
const rY = fooFunction("x"); // Error as desired, doesn't fit the constraint
// ^? (irrelevant, it's an error)
Playground link (Movie
version)
Or if you want the "other" type to be any array type, use any[]
instead of number[]
or Movie[]
:
const fooFunction = <T extends Foo | any[],>(arr: T): MaybeFoo<T> => arr as MaybeFoo<T>;
Then this test case changes from a desired error to success with type [string]
.
const rX = fooFunction(["x"]); // <== Now this one is allowed
// ^? const rX: [string]