1

This is about using a type from a 3P lib.

I was hoping to know if there was a quick way to narrow all of a known subtree so that I don't need to assert each field and subfield of interest individually?

interface User {
  id: string;
  private?: Private;
}
interface Private {
  info?: Info;
  images?: ImageData[];
  // ...
}
interface Info {
  name?: string;
  foo?: Foo;
}
// etc

async function client_list(id: string, parts: string[]) {
  // retrieve some user data
  // if successful, is guaranteed to return a known tree
  // eg for parts == ["info"]
  const user = {
    id: id,
    private: {
      info: {
        foo: {
          bar: {
            some_number: 42
          }
        }
      }
    }
  } as User;
  return user;
}

(async () => {
  const user = await client_list("abcd", ["info"]);

  user.private.info.foo.bar.some_number; // private, info, foo and bar are possibly undefined
  // after somehow narrowing the subtree down to some_mumber, asserting each node is not undefined
  user.private.info.foo.bar.some_number; // everything would be fine
})();

working example here

Thank you in advance.

Ps: I think duplicating the interfaces but without optional attributes, would work fine, but that would mean duplicating a lot of them.

jcalz
  • 264,269
  • 27
  • 359
  • 360
yukii
  • 55
  • 5
  • Please consider modifying the code in this question so as to constitute a [mcve] which, when dropped into a standalone IDE like [The TypeScript Playground (link to code)](https://tsplay.dev/mA738w), clearly demonstrates the issue you are facing (so no syntax or other errors unless these are what your question is about). This will allow those who want to help you to immediately get to work solving the problem without first needing to re-create it. And it will make it so that any answer you get is testable against a well-defined use case. – jcalz May 18 '21 at 19:24
  • Thank you for reminding me. I have updated my question with a working snippet. – yukii May 18 '21 at 20:09

1 Answers1

1

You can use recursive mapped and conditional to take an object type T and a tuple KS corresponding to an index path down that object, and convert it to a new type where the subtree of T at path KS is replaced with a version of itself where all properties and subproperties are required and not optional.

But it's not particularly pretty.


If all you wanted to do was take T and turn it into a version where all its properties, subproperties, sub-subproperties, etc. are required and not optional, you could do that fairly easily:

type DeepRequired<T> =
  T extends Function ? T : { [K in keyof T]-?: DeepRequired<T[K]> }

Here we are leaving functions alone (mapped types don't do nice things to functions, so it's best not to modify them); for non-function types, we are just using the -? mapped type modifier to turn any possibly-optional properties into required properties while we recurse downward.

Let's see what this does to one of your interfaces:

type DeepRequiredInfo = DeepRequired<Info>
/* type DeepRequiredInfo = {
    name: string;
    foo: {
        bar: {
            some_number: number;
        };
    };
} */

You can see that both the name and foo properties of DeepRequired<Info> are required. The type of name is string, while the type of foo is DeepRequired<Foo>, which has its properties made required, and so-on.

Note that DeepRequired<ImageData> will also traverse its properties downward, making them all required, which might not be what you want. But I'm going to consider doing anything different there out of scope for the question. For now, it's just a potential caveat.


Of course DeepRequired is not quite what you want; you want something like RequireSubtree<User, ["private", "info"]> where only the private property of User is made required, and only the info property of its private property is made required, and that property is of type `DeepRequired.

Well, here's how one might do it. First, it's useful to write a type function called Expand<T> which takes ugly intersections of object types like like {a: string} & {b: number} and turns them into equivalent single object types like {a: string; b: number}, and also tends to turn nested type functions like Baz<Qux<Fnord>> into an equivalent object type with expressly-written-out properties; see this question for more information:

type Expand<T> = T extends infer U ? { [K in keyof U]: T[K] } : never;

Now comes the fun:

type RequireSubtree<T, KS extends readonly any[]> =
  T extends Function ? (
    T
  ) : (
    KS extends readonly [infer K, ...infer R] ? (
      [K] extends [never] ? T :
      [K] extends [keyof T] ? (
        Expand<
          Omit<T, K> &
          { [P in K]-?: Exclude<RequireSubtree<T[P], R>, undefined> }
        >
      ) : (
        T
      )
    ) : (
      DeepRequired<T>
    )
  );

Again, if T is a function type, we don't want to alter it.

Then we look at the path tuple KS. If it is not empty, we take its first element K, and hold on to the rest of the tuple R. If K is a key of T, then we want to take T without the K subtree (using the Omit<T, K> utility type), and intersect it with a version of the K subtree whose property keys are all required, whose property values have had undefined Excluded from them, and whose property values have been recursive run through RequireSubTree using the rest of the path tuple R.

If the path tuple is empty, then we've finally gone down the full path and we can return DeepRequired<T>.

There are some weird edge cases being handled in there I won't get into, and there are probably even weirder edge cases that are not handled that I also won't get into. So there are caveats galore.


But now let's make sure it does the right thing:

type UserWithDeepRequiredPrivateInfo = RequireSubtree<User, ["private", "info"]>
/* type UserWithDeepRequiredPrivateInfo = {
    id: string;
    private: {
        images?: ImageData[] | undefined;
        info: {
            name: string;
            foo: {
                bar: {
                    some_number: number;
                };
            };
        };
    };
} */

That looks good; the private property is required, and its type a deeply required info subtree, but the images property is still optional, as desired.


Now we can give a type signature to your client_list function:

async function client_list<K extends keyof Private>(id: string, parts: K[]):
  Promise<RequireSubtree<User, ["private", K]>> {
  throw new Error("needs to be properly implemented");
}

The parts list seems to be an array of keys of Private, and the return type of client_list is a Promise of RequireSubtree<User, ["private", K]>>. Note that implementing this function properly will require care, since the compiler cannot possibly verify that anything is assignable to Promise<RequireSubtree<User, ["private", K]>> for a generic K; you'll need a type assertion or something like it to suppress the compiler error.


Anyway, we can finally call client_list and see how it works:

const user = await client_list("abcd", ["info"]);
/* const user: const user: {
  id: string;
  private: {
      images?: ImageData[] | undefined;
      info: {
          name: string;
          foo: {
              bar: {
                  some_number: number;
              };
          };
      };
  };
 } */
user.private.info.foo.bar.some_number; // okay
user.private.images.map(iD => iD.height.toFixed(2)); // error, possibly undefined

That looks good; the info property has a fully required subtree while the images property is still possibly undefined.

Compare this to the following:

const otherUser = await client_list("efgh", ["images"]);
otherUser.private.info.foo.bar.some_number; // error, possibly undefined
otherUser.private.images.map(iD => iD.height.toFixed(2)); // okay

const thirdUser = await client_list("ijkl", ["images", "info"]);
thirdUser.private.info.foo.bar.some_number; // okay
thirdUser.private.images.map(iD => iD.height.toFixed(2)); // okay

const nilUser = await client_list("mnop", []);
nilUser.private.info.foo.bar.some_number; // error, possibly undefined
nilUser.private.images.map(iD => iD.height.toFixed(2)); // error, possibly undefined

I think that's the desired behavior, right?


Note that if I thought you were only ever going to use a subtree path of one or two levels deep, I probably would not suggest a fully recursive RequiredSubtree. Instead I'd probably just use DeepRequired and a bit of manual typing for private and info. But I assume that these are just examples, and that your actual use case might have the required subtree at some arbitrary depth.

Finally, you should think about whether a fully recursive mapped conditional type is worth the complexity; the manual version you were talking about is tedious, but conceptually simple; anyone looking at your custom interfaces will probably be able to understand what they do and how to debug it if something goes wrong, whereas someone looking at RequiredSubtree<Foo, ["bar", "baz", "qux"]> and trying to figure out why it's not exactly what they expect might be in for a rough time.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360