Home > Mobile >  How to correctly type and cast a generic in TypeScript?
How to correctly type and cast a generic in TypeScript?

Time:10-26

I don't know how to ask the question correctly, since I don't understand how typing works with generics in TypeScript, but I know how to explain it, for example, assuming I have a "function store":

const functionStore: Array <Function> = [];

And I have a function to add functions to the function store, this function allows to receive an object and specify the name of the keys of the functions that will be added to the object that is passed by parameter:

const addFunction = <Obj = { [index: string]: any }>(
    obj: Obj,
    ...extractFunction: Array<keyof Obj>
) => {
    for (const key of extractFunction) {
        functionStore.push(obj[key]);
    }
};

Therefore, if I create the following object and pass it to addFunction:

const obj = {
    msg: 'Welcome!',
    test() {
        console.log('Hello!!');
    },
    doNoAdd() {
        console.log('Do not add!');
    },
    otherTest() {
        console.log('Other test!!');
    }
};

addFunction(obj, 'test', 'otherTest');

This doesn't work, as "Argument of type 'Obj[keyof Obj]' is not assignable to parameter of type 'Function'":

...
for (const key of extractFunction) {
    functionStore.push(obj[key]); //<-- Error!!
}
...

And if I do a casting, it will still give an error, since being a generic, that property can be a string, number, array, etc. (And I think there might be a better alternative instead of casting to unknown or any first):

...
functionStore.push(obj[key] as Function); //<-- Conversion of type 'Obj[keyof Obj]' to type 
                                          //     'Function' may be a mistake because neither
                                          //     type sufficiently overlaps with the other. 
                                          //     If this was intentional, convert the
                                          //      expression to 'unknown' first.
...

How can I do a typing in which it is specified that these keys, in addition to being a key of the obj, I can specify that it refers to a Function inside the object obj?

I hope you can help me :)

CodePudding user response:

The issue is in the way the generic is defined, the = operator assigns a default type which will probably never happen because it is always inferred by TS from the argument of type Obj. What would fix it here is to use the extend operator which will constrain the argument to extend the interface provided.

like this:

// this is pretty cool
const functionStore: Array <Function> = [];

// so it should extend the interface that you have defined
const addFunction = <Obj extends { [index: string]: any }>(
    obj: Obj,
    ...extractFunction: Array<keyof Obj>
) => {
    for (const key of extractFunction) {
        functionStore.push(obj[key]);
    }
};

That should do it - playground


Just to add, if you take a look at the playground link and hover over the addFunction call you will notice the generic type is inferred by the argument you provided. Here is the documentation on Generic Constraints.

CodePudding user response:

In order to make it a bit safer, you can add more constraints to Obj.

type Fn = (...args: any[]) => any

const functionStore: Array<Fn> = [];


type ExtractFn<T> = {
    [Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]

const addFunction = <Obj extends Record<string, any>>(
    obj: Obj,
    ...extractFunction: Array<ExtractFn<Obj>>
) => {
    for (const key of extractFunction) {
        functionStore.push(obj[key]);
    }
};
const obj = { name: 'John', getName: () => 'John' }

addFunction(obj, 'getName') // ok
addFunction(obj, 'name') // expected error

Playground As you might have noticed, you are not allowed to provide a key which is not corresponds to function.

  • Related