简体   繁体   中英

Is there a way to create a type-guard for nested key access in Typescript?

I am reworking an implementation which was previously made using vanilla Javascript. I ran into an interesting situation which I do have a solution for, but for which I thought I had a superior solution which does not appear to work.

Basically, I have an object with two nested objects, their indexes typed as the string literals of their keys, where both objects have some of the other objects keys but not a total overlap. Then, I have a function that can receive either key, and a second key to access one of the values in that object. I created custom type-guards for the key, and created a second set of type-guards to confirm the second key passed was a key of one of the objects. Then, I created a function that takes both keys, and should return an object with correctly matched keys. However, typescript does not seem convinced within the function that I am using my validator function, that the key returned from my function is only one that can definitely access the key of the upper level object.

That a really unclear explanation, and I think some example code will make it better, so here it is:

const decoder = {
    foo: {
      foo: "bar",
      bar: "foo"
    },
    bar: {
      foo: "bar",
    },
  };

type FooKeys = keyof typeof decoder["foo"];

type BarKeys = keyof typeof decoder["bar"];

const isFooKey = (key: string): key is FooKeys => Object.prototype.hasOwnProperty.call(decoder["foo"], key);

const isBarKey = (key: string): key is BarKeys => Object.prototype.hasOwnProperty.call(decoder["bar"], key);

const isFoo = (key: string): key is "foo" => key === "foo";

const isBar = (key: string): key is "bar" => key === "bar";

const validator = (key: string, secondKey: string) => {
if (isFoo(key) && isFooKey(secondKey)) {
    return { key, secondKey }
}

if (isBar(key) && isBarKey(secondKey)) {
    return { key, secondKey }
}

return false;
}

// Here comes where the issue arises
const someFunc = (key: string, nestedKey: string) => {
const validated = validator(key, nestedKey);

if (validated) {
    return decoder[validated.key][validated.secondKey];
}

return null;
}

Can anyone explain to me why this isn't working in the way I would tihnk it should, is this a shortcoming of typescript or is it an issue with my reasoning or implementation? If someone has a better solution for my problem, I would love to hear it!

The underlying problem here is that validated is what I've been calling a correlated record type , and TypeScript doesn't really have good support for those. The issue is that the types of decoder[validated.key] and validated.secondKey are both union types; the former is of type { foo: string; bar: string; } | { foo: string; } { foo: string; bar: string; } | { foo: string; } { foo: string; bar: string; } | { foo: string; } and the latter is of type "foo" | "bar" "foo" | "bar" . TypeScript's type system is pretty much unable to represent the fact that there is a correlation between those.

In general, if I have two values of union types where each union type has two members, like declare const x: A | B; declare const y: C | D; declare const x: A | B; declare const y: C | D; , the pair [x, y] will be of a type similar to [A, C] | [A, D] | [B, C] | [B, D] [A, C] | [A, D] | [B, C] | [B, D] [A, C] | [A, D] | [B, C] | [B, D] . But you happen to know that, for example, if x is of type A , then y will be of type C and vice versa... because the type of x is correlated to the type of y . So [A, D] and [B, C] are impossible. And therefore [x, y] should only be of [A, C] | [B, D] [A, C] | [B, D] . But the compiler is unable to infer this by itself, so it complains about these impossible situations.

In your case, the compiler is unable to verify that decoder[validated.key][validated.secondKey] is going to be a valid indexing operation. It thinks that decoder[validated.key] might be { foo: string } while validated.secondKey might be "bar" . And so it complains.


There are a few things you can do to work around this. One is just to use a type assertion to tell the compiler not to worry. This is the least type safe but it doesn't change your runtime code:

(decoder[validated.key] as { foo: string, bar: string })[validated.key]

You've basically claimed that decoder[validated.key] is the intersection of both its possible types, so that you can safely index its "foo" | "bar" "foo" | "bar" property.


You could write redundant code to lead the compiler through both possibilities:

validated.key == "foo" ?
  decoder[validated.key][validated.secondKey] :
  decoder[validated.key][validated.secondKey]

Here it uses control flow analysis to narrow validated to the two possibilities, after which validated.key and validated.secondKey are no longer union types.


Those are the two main ways to deal with correlated records in general. For your code a third possibility suggests itself: since your validator() function actually goes through both possibilities separately, you can move the indexing into that function to take advantage of the control flow analysis:

const validatorAndDecoder = (key: string, secondKey: string) => {
  if (isFoo(key) && isFooKey(secondKey)) {
    return { key, secondKey, val: decoder[key][secondKey] }
  }
  if (isBar(key) && isBarKey(secondKey)) {
    return { key, secondKey, val: decoder[key][secondKey] }
  }
  return null;
}

const someFunc2 = (key: string, nestedKey: string) => {
  const validated = validatorAndDecoder(key, nestedKey);
  if (validated) {
    return validated.val
  }
  return null;
}

Okay, hope that helps; good luck!

Playground link to code

TL;DR

you can do this istead:

function pluck<T, K extends keyof T>(o: T, propertyName: K): T[K] {
    return o[propertyName];
}

function validator(obj: typeof decoder, key: string, secondKey: string) {
    if (isFoo(key) && isFooKey(secondKey)) {
        const item = pluck(obj, key);
        const subItem = pluck(item, secondKey);
        return subItem;
    }

    if (isBar(key) && isBarKey(secondKey)) {
        const item = pluck(obj, key);
        const subItem = pluck(item, secondKey);
        return subItem;
    }

    return null;
}

Explanation

Index types

With index types, you can get the compiler to check code that uses dynamic property names.

see here ( pluck )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM