Как реализовать TypeScript с глубоким частичным отображением, не нарушая свойства массива

Есть идеи, как можно рекурсивно применять TypeScript Partial mapped type к интерфейсу, не нарушая при этом никаких ключей с возвращаемыми массивами типами?

Следующие подходы не были достаточными:

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

Есть идеи?

ОБНОВЛЕНИЕ: Принятый ответ - лучшее и более общее решение пока.

Найден временный обходной путь, который включает пересечение типов и два сопоставленных типа следующим образом. Наиболее заметным недостатком является то, что вы должны предоставить переопределения свойств для восстановления загрязненных ключей, которые имеют типы возвращаемых массивов.

Например

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

Вот так

Ответ 1

ОБНОВЛЕНИЕ 2018-06-22:

Этот ответ был написан год назад, до того, как удивительная функция условных типов была выпущена в TypeScript 2.8. Так что этот ответ больше не нужен. Пожалуйста, смотрите новый ответ @krzysztof-kaczor ниже, чтобы узнать, как получить это поведение в TypeScript 2.8 и выше.


Хорошо, вот моя лучшая попытка сумасшедшего, но полностью общего решения (требующего TypeScript 2.4 и выше), которое может вам не стоить, но если вы хотите его использовать, будьте моим гостем:

Во-первых, нам нужна логическая логика на уровне типов:

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

Все, что вам нужно знать, это то, что тип IfElse<True,A,B> оценивается как A а IfElse<False,A,B> оценивается как B

Теперь мы определяем тип записи Rec<K,V,X>, объект с ключом K и типом значения V, где Rec<K,V,True> означает, что свойство является обязательным, а Rec<K,V,False> означает свойство необязательно:

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

На данный момент мы можем перейти к вашим типам User и DeepPartialUser. Давайте опишем общую UserSchema<R> где каждое свойство, которое нас интересует, является обязательным или необязательным, в зависимости от того, является ли R True или 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>

Гадкий, правда? Но мы можем наконец описать как User и DeepPartialUser как:

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

И увидеть это в действии:

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

Вот и ты. Надеюсь, это поможет!

Ответ 2

С TS 2.8 и условными типами мы можем просто написать:

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

Вы можете проверить пакет https://github.com/krzkaczor/ts-essentials для этого и некоторых других полезных типов.

Ответ 3

Вы можете использовать ts-toolbelt, он может выполнять операции над типами на любой глубине

В вашем случае это будет:

import {O} from 'ts-toolbelt'

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

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

И если вы хотите глубоко вычислить его (для целей отображения), вы можете использовать для этого Compute