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

前端 未结 4 2138
遇见更好的自我
遇见更好的自我 2020-12-24 01:16

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 fo

相关标签:
4条回答
  • 2020-12-24 01:55

    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

    0 讨论(0)
  • 2020-12-24 01:57

    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.

    0 讨论(0)
  • 2020-12-24 02:05

    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
    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 (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 and non-primative values
    };
    

    The NonAny type is used to check for any values

    0 讨论(0)
  • 2020-12-24 02:10

    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!

    0 讨论(0)
提交回复
热议问题