typescript - DeepPartial except for the nested object keys - Stack Overflow

admin2025-04-25  5

I have a TypeScript interface, and I want to pass it to a generic type that will return the same interface, where all keys are optional - except for nested object keys. Basically the same as DeepPartial while maintaining the structure.

The use-case I am facing is a building a configuration object that can have varying levels of nesting, and while building it, any final value can be undefined, but the object structure exists from the get-go.
In my case, I am only working with serializable JSON types (no class instances, symbols, etc.) and a "final value" is anything that is not an object (string, number, boolean, array, null)

interface Foo {
  nested: {
    val: string
  }
  arr: string[]
}

type DeepPartialWithStructure<T> = ...;

let x: DeepPartialWithStructure<Foo> = { nested: {} };
/**
 * {
 *   nested: {
 *     val?: string
 *   }
 *   arr?: string[]
 * }
 */

I know that I can do this:

type DeepPartialWithStructure<T> = {
  [K in keyof T]: T[K] extends object
    ? DeepPartialWithStructure<T[K]>
    : T[K] | undefined;
};

But this unfortunately does not satisfy the above use-case. It requires to explicitly set nested:.val to be undefined or it errors with Property 'val' is missing in type '{}' but required in type 'DeepPartialWithStructure<{ val: string; }>'.

I have a TypeScript interface, and I want to pass it to a generic type that will return the same interface, where all keys are optional - except for nested object keys. Basically the same as DeepPartial while maintaining the structure.

The use-case I am facing is a building a configuration object that can have varying levels of nesting, and while building it, any final value can be undefined, but the object structure exists from the get-go.
In my case, I am only working with serializable JSON types (no class instances, symbols, etc.) and a "final value" is anything that is not an object (string, number, boolean, array, null)

interface Foo {
  nested: {
    val: string
  }
  arr: string[]
}

type DeepPartialWithStructure<T> = ...;

let x: DeepPartialWithStructure<Foo> = { nested: {} };
/**
 * {
 *   nested: {
 *     val?: string
 *   }
 *   arr?: string[]
 * }
 */

I know that I can do this:

type DeepPartialWithStructure<T> = {
  [K in keyof T]: T[K] extends object
    ? DeepPartialWithStructure<T[K]>
    : T[K] | undefined;
};

But this unfortunately does not satisfy the above use-case. It requires to explicitly set nested:.val to be undefined or it errors with Property 'val' is missing in type '{}' but required in type 'DeepPartialWithStructure<{ val: string; }>'.

Share Improve this question edited Jan 20 at 11:34 Jason asked Jan 16 at 11:02 JasonJason 7441 gold badge7 silver badges19 bronze badges 4
  • Does this approach meet your needs? You just have a single example with only one level of subproperties so it's not obvious to me exactly what you're going for, and I can interpret your verbal description multiple ways. If I didn't guess correctly, please edit with a more clear description of what you're looking for, as well as a more representative use case. These deeply recursive types tend to have a lot of weird edge cases, so please test thoroughly. If it happens to be what you want I'll write an answer explaining. – jcalz Commented Jan 16 at 13:48
  • @jcalz that is actually exactly what I needed, thank you!. I didn't think of using x extends object ? never : x and the reverse ternary to append ? only to the fields I wanted. Beautiful solution. I did have a problem with arrays & functions but I fixed it - playground – Jason Commented Jan 19 at 9:42
  • Do arrays matter to the question as asked? That is, should I write up an answer with my current solution, or do you want to edit the question first to show a minimal reproducible example where it matters what happens to arrays? (And if so, what do you want DPWS<Foo[]> to be?). As I mentioned there are always weird edge cases, so personally I'd like to write up my original solution with the caveat that you need to probe for edge cases and deal with them as needed (as you've done). Let me know. – jcalz Commented Jan 19 at 17:15
  • @jcalz I'll edit my question with a little more details and you can answer appropriately :) – Jason Commented Jan 20 at 10:35
Add a comment  | 

1 Answer 1

Reset to default 1

It would be nice if TypeScript had the ability to conditionally apply mapping modifiers in mapped types, as requested in microsoft/TypeScript#32562. Then presumably you could turn on ? only for those properties containing "final values". In the absence of that, the best you can do is split the object into two pieces based on the condition, make one of them Partial, and then intersect them together. This is how answers to similar questions like TypeScript generic type: Makes all object's fields optional recursively, unless the field's type is array work.

So then conceptually all you need to do is

type DPWS<T> =
  { [K in keyof T as T[K] extends object ? K : never]: DPWS<T[K]> } &
  { [K in keyof T as T[K] extends object ? never : K]?: DPWS<T[K]> };

where I'm using key remapping to never to filter out keys you don't want.

But when you have arrays, this does some pretty ugly stuff, because it starts drilling down into array members like length and push and decides to make them conditionally optional. You don't want that; you'd like to map arrays to arrays. So we need a special case that detects arrays and maps them without splitting them:

type DPWS<T> = T extends readonly any[] ? { [I in keyof T]: DPWS<T[I]> } :
  { [K in keyof T as T[K] extends object ? K : never]: DPWS<T[K]> } &
  { [K in keyof T as T[K] extends object ? never : K]?: DPWS<T[K]> };

This now preserves arrays as arrays... but you've apparently decided that arrays are not objects (but, of course, they are objects in JS and TS). So that means our key filtering needs to check for arrays separately:

type DPWS<T> = T extends readonly any[] ? { [I in keyof T]: DPWS<T[I]> } :
  { [K in keyof T as
    T[K] extends object ? T[K] extends readonly any[] ? never : K : never
    ]: DPWS<T[K]> } &
  { [K in keyof T as
    T[K] extends object ? T[K] extends readonly any[] ? K : never : K
    ]?: DPWS<T[K]> };

And now we're effectively done:

type DPWSFoo = DPWS<Foo>;
/* type DPWSFoo = {
    nested: {} & {
        val?: string | undefined;
    };
} & {
    arr?: string[] | undefined;
} */

That's equivalent to the type you want, but... it's kind of ugly since it carries intersections around. Intersections of object types can be collapsed to equivalent single object types. So let's address that by taking the resulting type and pushing it through an identity mapped type (using conditional type inference to copy the resulting type into a type parameter, which makes mapping a lot easier):

type DPWS<T> = (
  T extends readonly any[] ? { [I in keyof T]: DPWS<T[I]> } :
  { [K in keyof T as
    T[K] extends object ? T[K] extends readonly any[] ? never : K : never
    ]: DPWS<T[K]> } &
  { [K in keyof T as
    T[K] extends object ? T[K] extends readonly any[] ? K : never : K
    ]?: DPWS<T[K]> }
) extends infer U ? { [K in keyof U]: U[K] } : never;

type DPWSFoo = DPWS<Foo>;
/* type DPWSFoo = {
    nested: {
        val?: string | undefined;
    };
    arr?: string[] | undefined;
} */

That looks a lot better.


That demonstrates the basic approach, but note that there is a single example here and you (this "you" might be a future reader, not necessarily OP) might have use cases where the above type doesn't do what you want (e.g., special behavior for functions, for union types, for types with index signatures, for types which already have optional properties, etc.) I'll consider those other use cases out of scope here, and encourage you to modify the type yourself if you need them. But note that deeply recursive types like DPWS tend to have weird edge case behavior, and sometimes fixing those requires a complete refactoring of the type. So, uh, beware.

Playground link to code

转载请注明原文地址:http://www.anycun.com/QandA/1745535953a90904.html