Given a class like this:
class Example {
always: number;
example?: number;
a?: {
b?: {
c?: number;
}
};
one?: {
two?: {
three?: number;
four?: number;
}
};
}
Is it possible to, for example, mark a.b.c and one.two.three as non-optional (required) properties, without changing example and possibly also without changing one.two.four?
I was wondering if there was some recursive version of MarkRequired from ts-essentials.
Use case:
We have a ReST-like API that returns data where some properties are always defined, and others are optional and explicitly-requested by the client (using a query string like ?with=a,b,c.d.e). We'd like to be able to mark the requested properties and nested properties as not including undefined, to avoid having to do unnecessary undefined checks.
Is something like this possible?
So here is what I came up with to create a recursive DeepRequired type.
Two generic type parameters:
T for the base type Example
P for an union type of tuples, that represent our "required object property paths" ["a", "b", "c"] | ["one", "two", "three"] (similar to lodash object paths via get)P[0]: "a" | "one"
We include all properties from Example and additionally create a mapped type to remove ? and the undefined value for each optional property that is to be changed to required. We can do that by using the built-in types Required and NonNullable.
type DeepRequired<T, P extends string[]> = T extends object
? (Omit<T, Extract<keyof T, P[0]>> &
Required<
{
[K in Extract<keyof T, P[0]>]: NonNullable<...> // more shortly
}
>)
: T;
T to iteratively get the next required sub property in the path. To do that, we create a helper tuple type Shift (more on the implementation shortly). type T = Shift<["a", "b", "c"]>
= ["b", "c"]
ShiftUnion capable to distribute unions of tuples over the conditional type containing Shift:type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]>
= ["b", "c"] | ["two", "three"]
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]>[0]
= "b" | "two"
Main type DeepRequired
type DeepRequired<T, P extends string[]> = T extends object
? (Omit<T, Extract<keyof T, P[0]>> &
Required<
{
[K in Extract<keyof T, P[0]>]: NonNullable<
DeepRequired<T[K], ShiftUnion<P>>
>
}
>)
: T;
Tuple helper types Shift/ShiftUnion
We can infer the tuple type, that is shifted by one element, with help of generic rest parameters in function types and type inference in conditional types.
// Analogues to array.prototype.shift
export type Shift<T extends any[]> = ((...t: T) => any) extends ((
first: any,
...rest: infer Rest
) => any)
? Rest
: never;
// use a distributed conditional type here
type ShiftUnion<T> = T extends any[] ? Shift<T> : never;
type DeepRequiredExample = DeepRequired<
Example,
["a", "b", "c"] | ["one", "two", "three"]
>;
declare const ex: DeepRequiredExample;
ex.a.b.c; // (property) c: number
ex.one.two.three; // (property) three: number
ex.one.two.four; // (property) four?: number | undefined
ex.always // always: number
ex.example // example?: number | undefined
Playground
There is still some minor inaccuracy left: If we add property two also under a, e.g. a?: { two?: number; ... };, it also gets marked as required, despite not beeing in our paths P with ["a", "b", "c"] | ["one", "two", "three"] in the example. We can fix that easily by extending the ShiftUnion type:
type ShiftUnion<P extends PropertyKey, T extends any[]> = T extends any[]
? T[0] extends P ? Shift<T> : never
: never;
Example:
// for property "a", give me all required subproperties
// now omits "two" and "three"
type T = ShiftUnion<"a", ["a", "b", "c"] | ["one", "two", "three"]>;
= ["b", "c"]
This implementation excludes equally named properties like two, that are in different "object paths". So two under a is not marked required anymore.
Playground
Hope, that helps! Feel free to use that as a base for your further experiments.
I augmented ford64's answer with template literal types to allow for specifying the paths using dot-separated strings, which looks a lot more familiar in syntax than arrays of keys. It's not 100% the same since you can't express a key with a . in it; square brackets dont work ([]); and you can express keys in a way that javascript wouldnt allow like a.b-c.d for obj.a[b-c].d; but these are pretty minor, and it would be pretty simple to augment this type to at least support the brackets case if someone really wanted it.
Here's a playground link demonstrating it! I edited the names of the types a bit, simplified some types and got rid of unnecessary uses of any, though I still don't understand how the ShiftUnion type from the previous answer works to solve the issues, so I left it.
Basically you take ford04's answer and just wrap the paths to require in the PathToStringArray type.
type PathToStringArray<T extends string> = T extends `${infer Head}.${infer Tail}` ? [...PathToStringArray<Head>, ...PathToStringArray<Tail>] : [T]
// ford04's answer, then
type DeepRequiredWithPathsSyntax<T, P extends string> = DeepRequired<T, PathsToStringArray<P>>
Result is you can use a dot-separated syntax to make these paths instead of wordy array syntax, like so:
type Foo = { a?: 2, b?: { c?: 3, d: 4 } }
type A = RequireKeysDeep<Foo, "a">; // {a: 2, b?: { c?: 3, d: 4 } }
type B = RequireKeysDeep<Foo, "b">; // {a?: 2, b: { c?: 3, d: 4 } }
type BC = RequireKeysDeep<Foo, "b.c">; // {a?: 2, b: { c: 3, d: 4 } }
type ABC = RequireKeysDeep<Foo, "a" | "b.c">; // {a: 2, b: { c: 3, d: 4 } }
Tests are in the playground link.
This worked well for me:
//Custom utility type:
export type DeepRequired<T> = {
[K in keyof T]: Required<DeepRequired<T[K]>>
}
//Usage:
export type MyTypeDeepRequired = DeepRequired<MyType>
The custom utility type takes any type and iteratively sets it's keys to required and recursively calls deeper structures and does the same. The result is a new type with all parameters on your deeply nested type set to required.
I got the idea from this post where he makes a deeply nested type nullable:
type DeepNullable<T> = {
[K in keyof T]: DeepNullable<T[K]> | null;
};
https://typeofnan.dev/making-every-object-property-nullable-in-typescript/
Thus, this method could be used to change arbitrary attributes of the deeply nested properties.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With