Búsqueda de sitios web

Genéricos de TypeScript, se está usando keyof, pero TS también calcula con symbol | number | string así como con


Vamos a pasar directamente al ejemplo.

En el código de abajo puedes ver keyof siendo usado, pero TypeScript aún calcula el tipo de EVT como string|number|string, incluso cuando EVT extiende keyof ((typeof Events)[OBJ])

CÓDIGO: (EN LA ÚLTIMA LÍNEA AUTOCOMPLETADO Y VERIFICACIÓN DE TIPO FUNCIONAN BIEN)

export const Events = {
    // [event: string]: (...args: any) => void;
    account: {
        available: (status?:  'online'|'offline',foo?:'test',bar?:'test2') => {
            console.error('1');
        },
    }
}

export  class AMQP {
    static event<
        OBJ extends keyof typeof Events,
        EVT extends keyof ((typeof Events)[OBJ]),
        FUNC extends ((typeof Events)[OBJ][EVT])
        >(
        obj: OBJ,
        event: EVT,
        ...args: Parameters<FUNC>
    ) {

    }
}

AMQP.event('account','available','online','test','test2');

Enlace del parque

PROBLEMA (BANDERA ROJA AL PASAR EL RATÓN SOBRE 'FUN')

Type 'FUNC' does not satisfy the constraint '(...args: any) => any'.
  Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][EVT]' is not assignable to type '(...args: any) => any'.
    Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][keyof { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ]]' is not assignable to type '(...args: any) => any'.
      Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string] | { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][number] | { ...; }[OBJ][symbol]' is not assignable to type '(...args: any) => any'.
        Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string]' is not assignable to type '(...args: any) => any'.(2344)

ACTUALIZACIÓN:::

Hasta ahora. Problema como sugirió @Maciej Sikora al forzar declarar FUN como función solucionó el error.

pero aquí hay otro ejemplo. donde funciona bien sin declaración forzada, la única diferencia es que Events no es profundo, así que funciona allí pero no cuando el objeto es un poco más profundo, todavía tengo curiosidad por saber las razones.

Enlace del parque

Respuesta #1

Restringir tipo mediante tipo condicional

Podemos solucionarlo diciendo explícitamente a TS que solo queremos funciones, aunque actualmente solo haya eso, parece que TS no puede reducirlo. Considera:

type E = typeof Events // for readability

export  class AMQP {
    static event<
        OBJ extends keyof E,
        EVT extends keyof E[OBJ],
        FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ?  E[OBJ][EVT] : never
        >(
        obj: OBJ,
        event: EVT,
        ...args: Parameters<FUNC>
    ) {

    }
}

El punto principal es - FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ? E[OBJ][EVT] : never. Significa que solo permitimos que se seleccionen funciones en el segundo nivel de la estructura del objeto. Ahora parece que TS ya no se queja.

Otra ventaja de tal solución es que si proporcionamos un campo que no será una función, no habrá posibilidad de llamar a la función con esa opción.

Por qué TS no puede reducir el tipo

Hay algunas razones que pueden explicarlo parcialmente. Considera un ejemplo similar:

const a = {
    a: 2
}
const f = <A extends typeof a, K extends keyof A>(a: A, k: K) => {
    return a[k] + 1; // error we cannot use +
}
f(a, 'a');

Cómo es si no podemos usar + si a solo tiene un valor numérico dentro. ¿Parece que está mal? No, es correcto como nuestra función dice, permitimos en el tipo que extiende typeof a, así que no es lo mismo que a, podemos tener un objeto que tiene la propiedad a pero también otra propiedad b que será por ejemplo booleana. Considera que el siguiente código se compila:

const b = {
    a: 2,
    b: true,
}
f(b, 'b');

Así que puedo usar un objeto que estructuralmente tenga los mismos campos - a pero también tener otro con tipos diferentes. Por la misma razón, tu código no puede restringir el tipo, ya que podemos introducir otro objeto que será estructuralmente igual, pero tendría otras propiedades que no son funciones.

En nuestro ejemplo tenemos un caso un poco diferente, ya que nos referimos a un único objeto. Entonces no está completamente claro por qué en algún nivel anidado la inferencia falla. TS necesita ayuda adicional para limitar a la función. Por qué sucede esto para mí es desconocido, parece limitaciones de inferencia.

Introduce un tipo estático para Eventos

La segunda solución que deberías considerar es la tipificación estática de la interfaz Events. Considera:

type Events = Record<string, Record<string, (...a: any[]) => any>>
export const events = {
    account: {
        available: (status?: 'online' | 'offline', foo: 'test', bar: 'test2') => {
            console.error('1');
        },
    }
};

export  class AMQP {
    static event<
        INSTANCE extends Events,
        OBJ extends keyof INSTANCE,
        EVT extends keyof INSTANCE[OBJ],
        FUNC extends INSTANCE[OBJ][EVT]
        >(
        inst: INSTANCE,
        obj: OBJ,
        event: EVT,
        ...args: Parameters<FUNC>
    ) {

    }
}
// pay attention below I pass events object explicitly as first argument
AMQP.event(events, 'account', 'available', 'online', 'test', 'test2' );

Presta atención a que he introducido algunas cosas aquí:

  • Tipo de eventos
  • argumento adicional primero que es el objeto de instancia de la interfaz Events

Esta solución es más flexible, ya que la implementación no está vinculada a un objeto, puedes pasar cualquier objeto que sea estructuralmente correcto con la interfaz Events, además no hay necesidad de tipos condicionales.