How to implement TypeScript deep partial mapped type not breaking array properties
Asked Answered
D

5

43

Any ideas as to how might apply TypeScript's Partial mapped type to an interface recursively, at the same time not breaking any keys with array return types?

The following approaches have not been sufficing:

interface User {  
  emailAddress: string;  
  verification: {
    verified: boolean;
    verificationCode: string;
  }
  activeApps: string[];
}

type PartialUser = Partial<User>; // does not affect properties of verification  

type PartialUser2 = DeepPartial<User>; // breaks activeApps' array return type;

export type DeepPartial<T> = {
  [ P in keyof T ]?: DeepPartial<T[ P ]>;
}

Any ideas?

UPDATE: Accepted answer - A better and more general solve for now.

Had found a temporary workaround which involves intersection of types and two mapped types as follows. The most notable drawback is that you have to supply the property overrides to restore sullied keys, the ones with array return types.

E.g.

type PartialDeep<T> = {
  [ P in keyof T ]?: PartialDeep<T[ P ]>;
}
type PartialRestoreArrays<K> = {
  [ P in keyof K ]?: K[ P ];
}

export type DeepPartial<T, K> = PartialDeep<T> & PartialRestoreArrays<K>;

interface User {  
 emailAddress: string;  
 verification: {
   verified: boolean;
   verificationCode: string;
 }
 activeApps: string[];
}

export type AddDetailsPartialed = DeepPartial<User, {
 activeApps?: string[];
}>

Like so

Distrustful answered 28/7, 2017 at 11:22 Comment(7)
Looks like you need mapped conditional types which are not yet part of TypeScript πŸ™. If you want that fleshed out as an answer, let me know. – Speos
understood. what would you suggest as a solution for now? – Distrustful
The most straightfoward answer is to just manually declare a DeepPartialUser interface with what you want (a.k.a., give up). Or, you could do something like interface DeepPartialUser extends DeepPartial<User> { activeApps?: string[]; } which protects the particular array that broke while leaving the rest alone. – Speos
giving up entails maintaining two untethered data model elements, with no way of knowing how they are related. i would rather retrieve a partialed model from the base model, ensuring a sort of single source of truth for derived interfaces – Distrustful
found a hacky workaround based on your suggestion as shown in the updated question, what do you think of it. surely can be improved upon, no? – Distrustful
Yes, you can do that. I don't know if there's anything better, exactly. I've been trying to come up with a type-level witness that guarantees that two manually specified types are related, and the best I can do is guarantee that they have the same keys and that one is a subtype of another. – Speos
ok, please let me know if you happen to find something better. also realised that keys with Date return types are also affected by the DeepPartial. – Distrustful
P
91

With TS 2.8 and conditional types we can simply write:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T[P] extends ReadonlyArray<infer U>
      ? ReadonlyArray<DeepPartial<U>>
      : DeepPartial<T[P]>
};

or with [] instead of Array<> that would be:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends (infer U)[]
    ? DeepPartial<U>[]
    : T[P] extends Readonly<infer U>[]
      ? Readonly<DeepPartial<U>>[]
      : DeepPartial<T[P]>
};

You might want to checkout https://github.com/krzkaczor/ts-essentials package for this and some other useful types.

Piranesi answered 20/4, 2018 at 7:51 Comment(10)
Do we not also need a extends object condition so that primitives aren't themself mapped? – Ultramontane
You shouldn't need too; with let foo: Partial<string> all the methods and length are not optional. and foo is ultimately typed as a primitive. – Cockaigne
I've found out that if we try to get a partial of types written in a *.d.ts file, this solution fails. Instead of the specific type, it gets mapped into a DeepPartial of any (it becomes DeepPartial<any> | DeepPartial<{}>[] | ReadonlyArray<DeepPartial<{}>>). However, if I put the same declarations inside a *.ts file, it gets typed correctly. – Again
@SuhairZain you should open an issue on typescript repo :) – Perceptive
That's a funny use of "simply"! :D – Politesse
Similar to the comment by @SuhairZain, if you are using an index type whose value is any this will also break. I.e., { [key: string]: any }. – Selfish
How to make this work for index signature types? E.g. { query: { [k: string]: any } }. – Sufferance
Fix to make this work with any types: type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;. github.com/Microsoft/TypeScript/issues/30082 – Sufferance
To fix the any problem, just add unknown extends T ? T : at the beginning of the definition of DeepReadonly<T>. Type unknown is only assignable to types unknown and any, so if unknown extends T, that means T is either unknown or any. DeepReadonly<unknown> and DeepReadonly<any> now both resolve to unknown and any, respectively. – Garonne
Right now (TS 3.9.5) it seems that this type "poison" properties like myProp: T[] type making them myProp: (T|undefined)[] | undefined instead of myProp: T[] | undefined, has anyone ideas on how to fix this behaviour? – Harmon
S
13

UPDATE 2018-06-22:

This answer was written a year ago, before the amazing conditional types feature was released in TypeScript 2.8. So this answer is no longer needed. Please see @krzysztof-kaczor's new answer below for the way to get this behavior in TypeScript 2.8 and up.


Okay, here is my best attempt at a crazy but fully general solution (requiring TypeScript 2.4 and up) which might not worth it to you, but if you want to use it, be my guest:

First, we need some type-level boolean logic:

type False = '0'
type True = '1'
type Bool = False | True
type IfElse<Cond extends Bool, Then, Else> = {'0': Else; '1': Then;}[Cond];

All you need to know here is that the type IfElse<True,A,B> evaluates to A and IfElse<False,A,B> evaluates to B.

Now we define a record type Rec<K,V,X>, an object with key K and value type V, where Rec<K,V,True> means the property is required, and Rec<K,V,False> means the property is optional:

type Rec<K extends string, V, Required extends Bool> = IfElse<Required, Record<K, V>, Partial<Record<K, V>>>

At this point we can get to your User and DeepPartialUser types. Let's describe a general UserSchema<R> where every property we care about is either required or optional, depending on whether R is True or False:

type UserSchema<R extends Bool> =
  Rec<'emailAddress', string, R> &
  Rec<'verification', (
    Rec<'verified', boolean, R> &
    Rec<'verificationCode', string, R>
  ), R> &
  Rec<'activeApps', string[], R>

Ugly, right? But we can finally describe both User and DeepPartialUser as:

interface User extends UserSchema<True> { } // required
interface DeepPartialUser extends UserSchema<False> { }  // optional

And see it in action:

var user: User = {
  emailAddress: '[email protected]',
  verification: {
    verified: true,
    verificationCode: 'shazam'
  },
  activeApps: ['netflix','facebook','angrybirds']
} // any missing properties or extra will cause an error

var deepPartialUser: DeepPartialUser = {
  emailAddress: '[email protected]',
  verification: {
    verified: false
  }
} // missing properties are fine, extra will still error

There you go. Hope that helps!

Speos answered 28/7, 2017 at 18:40 Comment(1)
You are right, crazy, but fully general and definitely worth it. I do hope typescript adds mapped conditional types asap. – Distrustful
J
2

I started with @krzysztof's answer but have since been iterating on it when I come across edge cases. Specifically the edge cases below, based on the given value of the base object (i.e. T[P]):

  • any
  • any[]
  • ReadonlyArray<any>
  • Map
  • Set
  • Date
type NonAny = number | boolean | string | symbol | null;
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends NonAny[] // checks for nested any[]
    ? T[P]
    : T[P] extends ReadonlyArray<NonAny> // checks for nested ReadonlyArray<any>
    ? T[P]
    : T[P] extends Date // checks for Date
    ? T[P]
    : T[P] extends (infer U)[]
    ? DeepPartial<U>[]
    : T[P] extends ReadonlyArray<infer U>
    ? ReadonlyArray<DeepPartial<U>>
    : T[P] extends Set<infer V> // checks for Sets
    ? Set<DeepPartial<V>>
    : T[P] extends Map<infer K, infer V> // checks for Maps
    ? Map<K, DeepPartial<V>>
    : T[P] extends NonAny // checks for primative values
    ? T[P]
    : DeepPartial<T[P]>; // recurse for all non-array, non-date and non-primative values
};

The NonAny type is used to check for any values

Jorgenson answered 15/4, 2020 at 16:25 Comment(0)
C
0

You can use ts-toolbelt, it can do operations on types at any depth

In your case, it would be:

import {O} from 'ts-toolbelt'

interface User {  
    emailAddress: string;  
    verification: {
      verified: boolean;
      verificationCode: string;
    }
    activeApps: string[];
}

type optional = O.Optional<User, keyof User, 'deep'>

And if you want to compute it deeply (for display purposes), you can use Compute for that

Copeland answered 19/6, 2019 at 20:56 Comment(0)
G
0

This seems to work fine.

type DeepOptional<T> = 
  T extends Date | Function ? T 
  : T extends (infer R)[] ? DeepOptional<R>[]
  : T extends Record<PropertyKey, any> ? 
    {
      [K in keyof T]?: DeepOptional<T[K]>
    } 
  : T;

See the playground

Generation answered 31/3, 2022 at 9:15 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.