piotrwitek / utility-types

Collection of utility types, complementing TypeScript built-in mapped types and aliases (think "lodash" for static types).
MIT License
5.54k stars 230 forks source link

Deep Recursive Optional & Required with the possibility to define a path to a property #196

Open robkuz opened 6 months ago

robkuz commented 6 months ago

Is your feature request related to a real problem or use-case?

Add a deep recursive Optional type and its inverse a deep recursive Required type both of which should work with providing path definitions into a deeply nested type and only switch those properties to optional or required that are defined as keys.
This feature is particular helpful if you have deeply nested types in a "workflow" like environment where as your object moves across the system more and more properties become mandatory at each workflow step.

Describe a solution including usage in code example

I have implemented the whole thing for the DeepRecursiveOptional already The reverse DeepRecursiveRequired should be relatively easy thou.

//need to use Pick2 instead of Pick because Pick only works with keyof T
type Pick2<T, Path extends string> = {
    [K in keyof T]: K extends Path
    ? T[K]
    : never;
};

//Helper to remove never properties which will be generated by Pick2
//as it works with strings and not the keyof operator
type RemoveNeverProperties<T> = {
    [K in keyof T as T[K] extends never ? never : K]: T[K];
};

//we first omit the properties that are to be optional & then we pick those
//remove any never props and make the result Partial
//at last we join the remainder with the newly partial properties
type Optional2<T extends object, K extends string> =
    Omit<T, K> & Partial<RemoveNeverProperties<Pick2<T, K>>>;

//Helper to check if a property needs to be handled recursevely
//and Helper that cuts the prefix for the next nesting level
type ShiftPath<Key extends PropertyKey, Path extends PropertyKey> =
    Path extends `${Extract<Key, string>}.${infer Rest}` ? Rest : never;

//Putting everything together
export type DeepRecursiveOptional<T, Path extends string> = 
    //make the defined props optional if they match on this nesting level
    Optional2<{
        //iterate thru each prop
        [K in keyof T]:
            //check if a "path" points into the next nesting level
            ShiftPath<K, Path> extends never
            //if not return the property without any change
            ? T[K]
            //else recursively apply optionality
            : DeepRecursiveOptional<T[K], ShiftPath<K, Path>>;
    }, Path>;

type A = {
    prop_a: string
    prop_b: string
    prop_c: {
        prop_d: boolean
        prop_e: number
        prop_f: {
            prop_g: Date
            prop_h: string
        }
    }
}

type B = DeepRecursiveOptional<A, "prop_a" | "prop_c.prop_d" | "prop_c.prop_f.prop_g">

const b: B = {
    //prop_a: "test",
    prop_b: "test",
    prop_c: {
        //prop_d: true,
        prop_e: 1,
        prop_f: {
            //prop_g: new Date(),
            prop_h: "test"
        }
    }
}

Who does this impact? Who is this for?

This is for typescript users

Describe alternatives you've considered (optional)

Having in mind a workflow like system with deeply neseted types ... the only way would be to always to rebuild the whole deeply nested type for each Workflow step. This introduces the possibility for eventually renaming a property and you loose the single source of truth in your code base.

Additional context (optional)

This solution is not as typesafe as one would wish for as I am operating with strings that define paths instead of using the keyof operator. Otoh it seems to me a rather insignificant drawback. If one defines a path into a (nested) property that does not exist due to a typo (let say "footbar" vs "foobar" ´) then the only thing that happens is that at the usage site the property will be still required instead of optional and the compiler will tell that