I have some interfaces that all extend a base interface. e.g:
interface Base {
sharedKey : "a" | "b"
}
interface ChildOne extends Base {
sharedKey : "a",
keyOne : "child_one"
}
interface ChildTwo extends Base {
sharedKey : "b",
keyTwo : "child_two"
}
All the extended interfaces define a shared key that narrows the definition. I'm trying to implement a feature that I've seen in many IDEs, which typehints the keys of nested objects. Consider a function such as this:
function doStuff( sharedKey, privateKey ) { // Something here};
This function will accept a key that is a
or b
, and based on that key, will detect whether privateKey
is allowed to be keyOne
or keyTwo
.
For example this should be allowed:
doStuff( 'a', 'keyOne' ); // Allowed
But this should not:
doStuff( 'b', 'keyOne' ); // Error: keyOne does not exist on ChildTwo
Is this possible? I tried using template literals, but the problem is that the returned type from template literals was a string, and I can't use it as a Type.
Using Record<Unions['sharedKey'], Unions>
(where Unions
is a union of child interfaces ) provides some typehinting, which helps preventing keys that don't exist at all, but still allows using keys from other interfaces.
CodePudding user response:
One possible solution would be to use function overloads:
interface ChildOne {
sharedKey : "a",
keyOne : "child_one"
}
interface ChildTwo {
sharedKey : "b",
keyTwo : "child_two"
}
function doStuff( sharedKey: ChildOne['sharedKey'], privateKey: keyof ChildOne ):number
function doStuff( sharedKey: ChildTwo['sharedKey'], privateKey: keyof ChildTwo ):number
function doStuff( sharedKey: string, privateKey: string) {
// Something here
return 0;
};
doStuff( 'a', 'keyOne' ); // Allowed
doStuff( 'b', 'keyOne' ); // Error: keyOne does not exist on ChildTwo
CodePudding user response:
This can be done in an extensible manner with Extract
:
function doStuff<
SharedKey extends Union["sharedKey"],
PrivateKey extends Exclude<keyof Extract<Union, { sharedKey: SharedKey }>, "sharedKey">
>(sharedKey: SharedKey, privateKey: PrivateKey) { ... }
where Union
is a union of all the children.
You get the shared key that's being used, then use the key to extract the child from the union who has that key.
Once you have the right child, you get the keys of that child excluding "sharedKey" (unless you also want the sharedKey to show up in type hinting?).
In essence, this is basically the same idea as @PabloCarrilloAlvarez's answer expressed as generics.
doStuff("a", "keyOne") // good
doStuff("b", "keyOne") // error
doStuff("b", "keyTwo") // good
doStuff("a", "keyTwo") // error
You can try this method with a few example calls below: