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 symbol
s.
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.