Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 888
Allan Simoyi Avatar asked Jul 28 '17 11:07

Allan Simoyi


2 Answers

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.

like image 142
Krzysztof Kaczor Avatar answered Nov 10 '22 05:11

Krzysztof Kaczor


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!

like image 38
jcalz Avatar answered Nov 10 '22 07:11

jcalz