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
Exclude
d 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