I'm working in an existing JavaScript codebase. There is a class which exposes pre-defined functions (e.g. "copy", "paste") for utility. The class can be instantiated with "extension" functions, which allow users to register other utility functions for later use.
This code isn't typed, so I'm trying to add types to the signatures. But I'm having a lot of trouble with the function that gets a utility function by name (get(name)
). A minimised version of the code (with my attempt at adding types) is as follows:
class Operation {
private extensions: {[key: string]: () => void}
constructor(extensions: {[key: string]: () => void}) {
this.extensions = extensions;
}
get(name: string): () => void {
if (this[name]) {
return this[name].bind(this)
}
if (this.extensions[name]) {
return this.extensions[name].bind(this)
}
// default to copy
return this.copy.bind(this);
}
copy() {
console.log('copied');
}
paste() {
console.log('pasted');
}
}
const operation = new Operation({'cut': () => { console.log('cut'); }});
operation.get('cut')();
This fails because of this[name]
: "Element implicitly has an 'any' type because type 'Operation' has no index signature ts(7053)".
Since this function is meant to accept arbitrary strings (because of the overrides), I don't think I can avoid typing the function as get(name: string)
. I couldn't figure out from the TypeScript documentation how to use conditional types, e.g. get(name: string | keyof Operation)
, and I'm not convinced that's the right solution.
What is the best way to (re-)structure and type the get(name)
function with strict types, given that name
is not guaranteed to be a property of Operation
?
CodePudding user response:
Check (in JavaScript, not TypeScript) that the key being accessed is one of the ones directly on the class that you want to permit - eg copy
or paste
. Then, TS will automatically infer that such access is allowed.
get(name: string): () => void {
if (name === 'copy' || name === 'paste') {
return this[name].bind(this)
}
if (this.extensions[name]) {
return this.extensions[name].bind(this)
}
// default to copy
return this.copy.bind(this);
}