Search code examples
typescripttypeofkeyof

Calling functions by using indexers created by typeof keyof


I'm trying to understand in what way can I call static functions dynamically, accessing the functions' class using an indexer.

Consider this simple example:

Comparer.ts defines static functions:

class Comparer {
    static strings(a: string, b: string) {
        return a.localeCompare(b);
    }

    static numbers(a: number, b: number) {
        return a > b ? 1 : a < b ? -1 : 0;
    }
}

Comparers.ts defines the functions' "types", so to say:

enum Comparers {
    strings,
    numbers,
}

And in index.ts, I want to call the functions using the enum:

type ComparerKey = keyof typeof Comparer;

const result = Comparer[Comparers.strings as ComparerKey]("a", "b");
console.log(result);

However, I get the error:

This expression is not callable. Not all constituents of type 'Comparer | ((a: string, b: string) => number) | ((a: number, b: number) => 1 | -1 | 0)' are callable. Type 'Comparer' has no call signatures.ts(2349)

Conversion of type 'Comparers' to type '"prototype" | "strings" | "numbers"' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.ts(2352)

IIUC, the reason it doesn't work is because the keyof typeof operator decomposes the type into prototype (as well as strings and numbers), which is not callable.

  • Note that there's another way to approach this - and it's without using keyof typeof, but instead assign the enums with strings:

    enum Comparers {
         strings = "strings",
         numbers = "numbers",
    }
    const result = Comparer[Comparers.strings]("a", "b"); // no error now
    console.log(result);
    

Question: is it even possible to use the keyof typeof operator in this case to make the call work?


Solution

  • First of all, the enum with no explicit values will have unsigned integers as values:

    enum Comparers {
        strings, // will be 0
        numbers, // will be 1
    }
    

    Thus, trying to get the method of Comparer by using this enum doesn't make any sense, since there is nothing in the class that is 0 or 1

    The only valid option is to give the explicit values to the enum:

    enum Comparers {
      strings = 'strings',
      numbers = 'numbers',
    }
    

    If you want to have the type for the keys anyway you can manually remove the prototype from the union using built-in Exclude utility type:

    type Keys = Exclude<keyof typeof Comparer, 'prototype'>