Home > OS >  How to typehint the keys of a nested object in TypeScript?
How to typehint the keys of a nested object in TypeScript?

Time:05-05

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:

Playground

  • Related