How to implement TypeScript deep partial mapped type not breaking array properties
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.
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!