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

ⅰ亾dé卋堺 提交于 2019-11-30 06:03:26

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: 'foo@example.com',
  verification: {
    verified: true,
    verificationCode: 'shazam'
  },
  activeApps: ['netflix','facebook','angrybirds']
} // any missing properties or extra will cause an error

var deepPartialUser: DeepPartialUser = {
  emailAddress: 'bar@example.com',
  verification: {
    verified: false
  }
} // missing properties are fine, extra will still error

There you go. Hope that helps!

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]>
};

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

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!