【问题标题】:How to implement TypeScript deep partial mapped type not breaking array properties如何实现 TypeScript 深度部分映射类型而不破坏数组属性
【发布时间】:2018-01-04 10:46:06
【问题描述】:

关于如何将 TypeScript 的 Partial 映射类型递归地应用于接口,同时不破坏任何具有数组返回类型的键的想法?

以下方法还不够:

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

Like so

【问题讨论】:

  • 看起来你需要mapped conditional types,它们还不是 TypeScript 的一部分 ????。如果您希望将其充实作为答案,请告诉我。
  • 明白了。您现在有什么建议作为解决方案?
  • 最直接的答案是手动声明一个 DeepPartialUser 接口与你想要的(也就是放弃)。或者,您可以执行类似interface DeepPartialUser extends DeepPartial&lt;User&gt; { activeApps?: string[]; } 之类的操作,它可以保护损坏的特定阵列,同时保留其他阵列。
  • 放弃需要维护两个不受约束的数据模型元素,但无法知道它们之间的关系。我宁愿从基础模型中检索一个局部模型,确保派生接口的单一事实来源
  • 根据您的建议找到了一个 hacky 解决方法,如更新后的问题所示,您对此有何看法。当然可以改进,不是吗?

标签: typescript recursion partial mapped-types


【解决方案1】:

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

或者用[] 代替Array&lt;&gt; 那将是:

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

您可能需要查看 https://github.com/krzkaczor/ts-essentials 包以了解此类型以及其他一些有用的类型。

【讨论】:

  • 我们是否还需要一个extends object 条件以使基元本身不被映射?
  • 我发现如果我们尝试将部分类型写入 *.d.ts 文件,此解决方案将失败。它不是特定的类型,而是映射到任何的 DeepPartial(它变成DeepPartial&lt;any&gt; | DeepPartial&lt;{}&gt;[] | ReadonlyArray&lt;DeepPartial&lt;{}&gt;&gt;)。但是,如果我将相同的声明放在 *.ts 文件中,它会被正确输入。
  • “简单”的用法很有趣! :D
  • 类似于@SuhairZain 的评论,如果您使用的索引类型的值为any,这也会中断。即{ [key: string]: any }.
  • 修复以使其适用于any 类型:type DeepPartial&lt;T&gt; = T extends object ? { [K in keyof T]?: DeepPartial&lt;T[K]&gt; } : T;github.com/Microsoft/TypeScript/issues/30082
【解决方案2】:

我从@krzysztof 的答案开始,但当我遇到极端情况时,我一直在迭代它。特别是下面的边缘情况,基于基础对象的给定值(即T[P]):

  • any
  • any[]
  • ReadonlyArray&lt;any&gt;
  • 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
};

NonAny 类型用于检查 any

【讨论】:

    【解决方案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

    【讨论】:

      【解决方案4】:

      2018 年 6 月 22 日更新:

      这个答案是在一年前写的,在惊人的 conditional types 功能在 TypeScript 2.8 中发布之前。所以不再需要这个答案。请参阅下面的@krzysztof-kaczor 的new answer,了解在 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&lt;True,A,B&gt; 类型的计算结果为AIfElse&lt;False,A,B&gt; 的计算结果为B

      现在我们定义一个记录类型Rec&lt;K,V,X&gt;,一个具有键K和值类型V的对象,其中Rec&lt;K,V,True&gt;表示该属性是必需的Rec&lt;K,V,False&gt;表示属性是可选的

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

      此时我们可以获取您的UserDeepPartialUser 类型。让我们描述一个通用的UserSchema&lt;R&gt;,其中我们关心的每个属性要么是必需的,要么是可选的,这取决于RTrue 还是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>
      

      丑陋,对吧?但我们最终可以将UserDeepPartialUser 描述为:

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

      看看它的实际效果:

      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
      

      给你。希望对您有所帮助!

      【讨论】:

      • 你是对的,疯了,但完全一般,绝对值得。我确实希望打字稿尽快添加映射的条件类型。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-07-13
      • 1970-01-01
      • 2020-12-04
      • 2018-02-05
      • 2020-08-04
      • 2016-06-10
      • 1970-01-01
      相关资源
      最近更新 更多