Pesquisa de site

typescript - Aplicar que o tipo seja um subtipo de interface e estenda número


Eu estou trabalhando com essas interfaces

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

E eu estou escrevendo uma função auxiliar que, dado um caminho, incrementa o valor correspondente de uma entidade em uma coleção.

Exemplo :

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

E estas são minhas digitações atuais

    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

Para este exemplo, as tipagens são bastante detalhadas, pois são usadas para verificar os tipos do que é passado

As tipagens acima funcionam, o único problema é que um caminho para um tipo não numérico ainda é válido.

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

Mas o acima não aplica a restrição como esperado

Alguma ideia?

Resposta #1

Você precisa filtrar as chaves disponíveis pelo tipo de propriedade. Tomando como referência o tipo Omit, podemos usar tipos condicionais para criar um tipo que filtrará propriedades com base no tipo de propriedade.

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

Resposta #2

Eu tento não usar tipos condicionais a menos que eu não consiga resolver o problema sem eles (já que às vezes eles têm propriedades de inferência estranhas que eu não consigo compreender). Então, que tal essas tipagens em vez disso?

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;

Existe apenas C e K1 (e talvez K2). A ideia é que C deva ser um Collection<Entity & Something>, onde Something é ou um Record<K1, number> ou um Record<K1, Record<K2, number>>, dependendo de quantos elementos há em path. (Caso seja relevante, Record<K extends string, V> é definido como {[P in K]: V} e simplesmente significa "um objeto com chaves do tipo K e valores do tipo V").

Você pode verificar que ele se comporta como desejado:

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

Espero que isso ajude; boa sorte!