Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deep/recursive Required<T> on specific properties

Tags:

typescript

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?

like image 741
glen-84 Avatar asked Sep 07 '19 15:09

glen-84


3 Answers

So here is what I came up with to create a recursive DeepRequired type.

Input

Two generic type parameters:

  1. T for the base type Example
  2. 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)

Example flow

  1. Grab all required properties in top level P[0]: "a" | "one"
  2. Create intersection type/concatenation of required and non required object properties

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;
  1. The type must be somehow recursive for sub properties. That implies, we also have to find a way to "shift" types from the tuple 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"]
  1. Challenging thing is, we want to pass in an union of tuples (aka many required paths), not just one. We can make use of distributive conditional types for this and use another helper 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"]
  1. We then can get all required properties for the next sub paths by simply selecting the first index:
type T = ShiftUnion<["a", "b", "c"] | ["one", "two", "three"]>[0] 
       = "b" | "two"

Implementation

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;

Test

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


Some polish (Update)

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

Possible extensions

  • For single required properties pass in strings instead of tuple paths for convenience.
  • Current implementation is suitable for a few object paths to be marked required; if multiple nested sub properties from an object are to be selected, the solution could be extended to receive object literal types instead of tuples.

Hope, that helps! Feel free to use that as a base for your further experiments.

like image 95
ford04 Avatar answered Sep 22 '22 01:09

ford04


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.

like image 30
osdiab Avatar answered Sep 23 '22 01:09

osdiab


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.

like image 39
Emanuel Lindström Avatar answered Sep 23 '22 01:09

Emanuel Lindström