Typescript generic array mapping with functions registered from different sources

Viewed 161

I have a Backend that provides me with data, that could be of different types in the same Array. This data should be seperated and handled differently for each type.

This is a simplified Version of the used Code:

type ReturnTypeInferer<T> = T extends (a: object) => infer U ? U : never;

interface TypeMap {
    'a': (val: object) => number | null,
    'b': (val: object) => string | null,
}



const typeFunctions: TypeMap = {
    'a': () => null,
    'b': () => null
};

function isNotNull<T>(v: T | undefined | null): v is T {
    return v != null;
  }

 
function f<U extends keyof TypeMap>(type: U): ReturnTypeInferer<TypeMap[U]>[] {

    const applications: object[] = [];

    // Response definition
    const result = applications
        .map<ReturnTypeInferer<TypeMap[U]>>(typeFunctions[type])
        .filter(isNotNull);
    return result;
 }

The return type of f() is actually ok but Typescript does not like the function call in .map: typeFunctions[type] =>

Argument of type '((val: object) => number) | ((val: object) => string)' is not assignable to parameter of type '(value: object, index: number, array: object[]) => ReturnTypeInferer<TypeMap[U]>'. 
Type '(val: object) => number' is not assignable to type '(value: object, index: number, array: object[]) => ReturnTypeInferer<TypeMap[U]>'.
Type 'number' is not assignable to type 'ReturnTypeInferer<TypeMap[U]>'.ts(2345)

If I omit the .map's type argument, result evaluates to the first returntype within TypeMap. So now its number[], but if you switch the lines ist string[].

Is this a Bug?

1 Answers

I would consider this more of a design limitation of TypeScript than a bug. See microsoft/TypeScript#44049 for a similar issue.

The problem here is that while typeFunctions[type] is inferred to have the type TypeMap[U], the compiler does not maintain the generic U when evaluating what happens when you call typeFunctions[type] as a function. That would end up generating very complicated types inside generic functions, and the degraded compiler performance would outweigh the benefits of the relatively few situations when it would be useful.

What happens instead is that the compiler replaces U with its constraint, namely keyof TypeMap, and so it is seen as a union of function types. This thoroughly confuses the compiler when you call applications.map(), in ways that are probably not useful to go into.


Instead, my suggestion here is to use a type assertion to tell the compiler what it can't figure out by itself: that typeFunctions[type] should be treated as type (val: object) => ReturnTypeInferer<TypeMap[U]>). Like this:

function f<U extends keyof TypeMap>(type: U) {
    const applications: object[] = [];
    const result = applications
        .map(typeFunctions[type] as (val: object) => ReturnTypeInferer<TypeMap[U]>)
        .filter(isNotNull);
    return result;
}

Now it compiles with no error. Type assertions aren't the safest thing, since it's possible to accidentally lie to the compiler with your assertion and then you can have problems at runtime. But in cases like this where you know more about the types than the compiler does, it's pretty much the best you can do.

Playground link to code

Related