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