Home > OS >  Conditional generic works differently when passed generic function parameter than when directly pass
Conditional generic works differently when passed generic function parameter than when directly pass

Time:10-20

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]

Playground link

  • Related