【问题标题】:Enforce type is subtype of interface and extends number强制类型是接口的子类型并扩展编号
【发布时间】:2018-03-21 01:09:06
【问题描述】:

我正在使用这些接口

/**
 * Interface for entities
 */
export interface Entity extends Object {
    readonly id: EntityId;
}

/**
 * Interface for collection-based state
 */
export interface Collection<T extends Entity> extends Object {
    readonly entities: { [key: string]: T };
    readonly ids: EntityId[];
}

我正在编写一个辅助函数,它给定路径,增加集合中实体的相应值。

例子:

Collection.increment(myCollection, 'some-id', ['stats', 'total']);

这些是我目前的类型

    export function increment<
        C extends Collection<any>,
        E = C extends Collection<infer U> ? U : never,
        K1 extends keyof E = keyof E,
        V1 extends E[K1]= E[K1]
        >(collection: C, entityId: string, path: K1 | [K1]): C
    export function increment<
        C extends Collection<any>,
        E = C extends Collection<infer U> ? U : never,
        K1 extends keyof E = keyof E,
        V1 = Exclude<E[K1], void>,
        K2 extends keyof V1 = keyof V1,
        V2 extends V1[K2]= V1[K2]
        >(collection: C, entityId: string, path: K1 | [K1] | [K1, K2]): C

对于这个例子,打字非常冗长,因为它们被用来检查传入的类型

上述类型有效,唯一的问题是非数字类型的路径仍然有效。

I tried something like the below to force it to only allow numbers

export function increment<
    C extends Collection<any>,
    E = C extends Collection<infer U> ? U : never,
    K1 extends keyof E = keyof E,
    V1 extends E[K1]= E[K1]
    >(collection: C, entityId: string, path: K1 | [K1]): C
export function increment<
    C extends Collection<any>,
    E = C extends Collection<infer U> ? U : never,
    K1 extends keyof E = keyof E,
    V1 = Extract<Exclude<E[K1], void>, number>, // Note the Extract
    K2 extends keyof V1 = keyof V1,
    V2 extends V1[K2]= V1[K2]
    >(collection: C, entityId: string, path: K1 | [K1] | [K1, K2]): C

但上述并没有按预期强制执行约束

有什么想法吗?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    您需要按属性类型过滤可用键。从Omit 类型中获取提示,我们可以使用条件类型来创建一个类型,该类型将根据属性类型过滤掉属性。

    type FilterKeysByType<T, U> = ({[P in keyof T]: T[P] extends U ? P: never } & { [x: string]: never })[keyof T];  
    // If there is a single key, it must be a key of a number field
    export function increment<
        C extends Collection<any>,
        E = C extends Collection<infer U> ? U : never,
        K1 extends FilterKeysByType<E, number | undefined> = FilterKeysByType<E, number | undefined>,
        V1 extends Exclude<E[K1], void> = Exclude<E[K1], void>
    >(collection: C, entityId: string, path: K1 | [K1]): C
    // If there are two keys, the first key, can be any  type, but the second key must be the key of a number field 
    export function increment<
        C extends Collection<any>,
        E = C extends Collection<infer U> ? U : never,
        K1 extends keyof E = keyof E,
        V1 = Exclude<E[K1], void>,
        K2 extends FilterKeysByType<V1, number| undefined> = FilterKeysByType<V1, number| undefined>,
        V2 extends V1[K2]= V1[K2]
        >(collection: C, entityId: string, path: [K1, K2]): C
    
    // Usage
    declare var c : Collection<Comment>;
    
    interface Comment extends Entity{
        name: string;
        value: number;
        optValue?: number;
        subcomment: Comment;
        optSubcomment?: Comment;
    }
    
    increment(c, "", "value"); //ok
    increment(c, "", "optValue"); //ok
    increment(c, "", "name");// error
    increment(c, "", ["subcomment", "value"]); // ok
    increment(c, "", ["subcomment", "optValue"]); // ok
    increment(c, "", ["subcomment", "name"]); // error
    increment(c, "", ["optSubcomment", "value"]); // ok
    increment(c, "", ["optSubcomment", "optValue"]); // ok
    increment(c, "", ["optSubcomment", "name"]); // error
    

    【讨论】:

    • 10 倍的有趣和知识渊博的答案。这很好用,谢谢!
    • @Tarlen 更新了答案以支持可选的number 属性 (number|undefined)
    【解决方案2】:

    我尽量不打破条件类型,除非没有它们我无法解决问题(因为它们有时具有令人毛骨悚然的推理属性,我无法绕开我的大脑)。那么这些类型呢?

    declare function increment<
     C extends Collection<Entity & Record<K1,Record<K2,number>>>, 
     K1 extends string, 
     K2 extends string>(collection: C, entityId: string, path: [K1, K2]): C;
    declare function increment<
     C extends Collection<Entity & Record<K1, number>>, 
     K1 extends string>(collection: C, entityId: string, path: K1 | [K1]): C;
    

    只有CK1(可能还有K2)。这个想法是C 应该是Collection&lt;Entity &amp; Something&gt;,其中SomethingRecord&lt;K1, number&gt;Record&lt;K1, Record&lt;K2, number&gt;&gt;,这取决于path 中有多少元素。 (以防万一,Record&lt;K extends string, V&gt; 定义为 {[P in K]: V},它仅表示“具有 K 类型的键和 V 类型的值的对象”。)

    您可以验证它的行为是否符合要求:

    interface Ent extends Entity {
      stats: {
        total: number,
        color: string
      }
      height: number,
      name: string
    }
    
    declare const myCollection: Collection<Ent>;
    
    increment(myCollection, 'some-id', ['stats', 'total']); // okay
    increment(myCollection, 'some-id', 'height'); // okay
    increment(myCollection, 'some-id', ['height']); // okay
    
    increment(myCollection, 'some-id', ['stats']); // error
    increment(myCollection, 'some-id', ['stats', 'name']); // error
    increment(myCollection, 'some-id', ['name']); // error
    increment(myCollection, 'some-id', ['stats', 'color']); // error
    increment(myCollection, 'some-id', ['random']); // error
    

    希望有所帮助;祝你好运!

    【讨论】:

    • 虽然不是上面描述的一部分,但问题是当路径包含可选属性时它会失败。这些类型仍然适用于可选属性,因为如果路径不可访问,实现将提前返回
    • @Tarlen 如果您将记录替换为部分记录type PartialRecord&lt;K extends string, T&gt; = { [P in K]?: T; };,这几乎可以工作。这将仅限于数字属性,但允许随机属性。不过非常有趣的方法:)
    • 您能否详细说明您的句子“这将仅限于数字属性,但允许随机属性”? :)
    • @Tarlen。对于PartialRecord,jcalz 的所有测试用例都通过了答案,除了最后一个,他有一个随机的属性名称。我希望他有一个解决方案,我更喜欢他的方法。条件类型很酷,但他是对的,它们变得很奇怪,我倾向于太快地使用它们:)
    • 啊,明白了。是的,我同意条件类型需要一些时间来适应。 Jcalz 方法更容易理解,尤其是对于 TS 新手(包括我自己):)
    猜你喜欢
    • 2016-12-09
    • 1970-01-01
    • 2021-08-07
    • 2020-03-09
    • 2020-05-04
    • 1970-01-01
    • 1970-01-01
    • 2013-09-25
    • 2015-02-16
    相关资源
    最近更新 更多