Given the following object:
const testFunctions = {
test: () => 'test',
test2: ( yo: string ) => console.log('test2')
}
and this addCommands function that maps the same functions but appends the component name.
const addCommands = <N extends string, T extends Record<string, any>>(name: N, myFunctions: Readonly<T>) => {
const commands = {};
Object.keys(myFunctions).forEach((key) => {
const name = `${name}_${key}`;
commands[name] = myFunctions[key];
}
// Cypress.Commands.addAll(commands); // ignore this
return commands
}
when I call the addCommands function:
const mappedCommands = addCommands('testing', testFunctions );
the returned type should be of type:
/*
{
testing_test: () => string;
testing_test2: (yo: string) => void
}
*/
Any ideas?
CodePudding user response:
In what follows I'm only concerned with the call signature of addCommands(), not its implementation. That call signature should look like:
declare const addCommands: <N extends string, T extends Record<string, any>>(
name: N, myFunctions: T
) => { [K in (string & keyof T) as `${N}_${K}`]: T[K]; }
This is using key remapping in mapped types via as to convert each string-valued key K in the keys of T to a version prepended with the type of name and an underscore. Since name is of type N, the new key type is the template literal type `${N}_${K}`.
Note that if you just write {[K in keyof T as `${N}_${K}`]: T[K]} you get an error that K can't appear in a template literal type. That's because, in general, the keys you get from the keyof operator are some subtype of PropertyKey, and these include symbol-valued keys, which TypeScript doesn't want to let you serialize (after all, an actual template literal value would produce a runtime error if you tried to do that).
To prevent that error we can restrict the keys over which K ranges. One way is to intersect keyof T with string, like (string & keyof T), to get just the string-valued keys. You could write ((string | number) & keyof T) if you want to support number keys (so that {0: ""} gets mapped to {test_0} instead of {}). Or Exclude<keyof T, symbol> using the Exclude<T, U> utility type, et cetera. The point is to convince the compiler that you're not going to try to serialize any symbols.
Let's test it:
const testFunctions = {
test: () => 'test',
test2: (yo: string) => console.log('test2')
}
const mappedCommands = addCommands('testing', testFunctions);
/* const mappedCommands: {
testing_test: () => string;
testing_test2: (yo: string) => void;
} */
Looks good, that's the type you wanted.
