2

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?

1 Answer 1

2

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);
}
Sign up to request clarification or add additional context in comments.

2 Comments

This is great! But is there any way to do this so that the names do not have to be hard-coded in the function? i.e. if someone adds a new pre-defined function, they won't have to modify the contents of get()?
Technically, you can bypass anything with a type assertion, but I prefer to avoid those because they're less type-safe. I'd prefer to list out the properties individually. But you can do if (Operation.prototype.hasOwnProperty(name)) { return this[name as Exclude<keyof Operation, 'get'>].bind(this) } TypeScript doesn't work as elegantly as would be ideal with these sorts of overloaded functions with dynamic keys.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.