Reputation: 472
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
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!
Upvotes: 1