Hugo Aboud
Hugo Aboud

Reputation: 472

TypeScript: Enforcing class parameter types without interfering with keys

I need to define a schema of "operations" to be used on my application. This schema should be extendable for other groups of "operations". It should contain a dictionary of settings for each keyword.

At some point, a "generic caller" should receive the type of the "operation" plus the keyword and make a cache of it for later calls.

I also need this "generic caller" to ensure the requested keyword is defined on the operation at compile time, so it shows errors to the developers on VS Code.

Below, a solution that's very close to what I need:

// Operation interface
interface Operation {
    url: string
    parameters: Record<string,string>
}

// Operations schema
class Operations {}
class BaseOperations extends Operations {   
    one: {
        url: '/one',
        parameters: {p: '1'}
    }
    two: {
        url: '/two',
        parameters: {}
    }
}

// Generic caller (which caches the calls)
function runOperation<T extends Operations>(type: {new(): T;}, keyword: keyof T) {
    let operation = new type();
    //cache_and_run(operation[keyword]);
}

// Main
function main() {
    runOperation(BaseOperations, 'one');
    runOperation(BaseOperations, 'two');
    runOperation(BaseOperations, 'three'); // VS Code error, as expected
}

The only problem here is that the parameters defined in Operations are not bound the Operation interface. It's a minor issue, however I'd like to be able to make sure both ends (operations definitions and their uses) are checked at compile time.

After some research, I've found the "index signature" parameter, which allows for enforcing the returned type:

class BaseOperations extends Operations {
    [x:string]: Operation
    one: {
        url: '/one',
        parameters: {p: '1'}
    }
    two: { // 'uarl' is not assignable
        uarl: '/two'
    }
}

However, this approach disabled the 'one'|'two' check happening on runOperation, since any string is now a valid keyof BaseOperations.

Any suggestions?

Upvotes: 1

Views: 47

Answers (1)

jcalz
jcalz

Reputation: 327624

Instead of using an index signature, you can add an implements clause to your class declaration to ask the compiler to check that BaseOperations is assignable to the implemented type. In this case I would use a self-referential type, such as Record<keyof BaseOperations, Operation>:

class BaseOperations extends Operations 
    implements Record<keyof BaseOperations, Operation> {

    one!: {
        url: '/one',
        parameters: { p: '1' }
    }
    two!: { // error! 'uarl' is not assignable
        uarl: '/two'
    }

}

In the above there is an error at the two declaration. If you fix that then the class compiles without error. Meanwhile the compiler still knows exactly which keys exist on a BaseOperations because it has not widened its key set to string:

runOperation(BaseOperations, 'one'); // okay
runOperation(BaseOperations, 'two'); // okay
runOperation(BaseOperations, 'three'); // error!

Playground link to code

Upvotes: 1

Related Questions