Búsqueda de sitios web

Genéricos de Typescript, obtener el tipo de los contenidos de un Array


He creado un método que concatenará un arreglo con otro arreglo que se pasa. El arreglo se encuentra en un objeto padre llamado state, que se tipa de manera genérica en mi método.

Mi método es algo así:

export const mutateConcat = <T, K extends keyof T>(state: T, key: K, val: T[K]) => {
  ((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}

Obviamente esto es feo, y me gustaría deshacerme del casting a unknown y any[]. También me gustaría poder determinar el tipo del contenido del arreglo.

Básicamente, necesito asegurarme de que T[K] sea un Array, y idealmente me gustaría asignar el tipo de los contenidos del array a otro parámetro genérico, digamos U.

He intentado <T, K extends keyof T, U extends Array<T[K]>>, pero eso asigna U al tipo del propio arreglo, no al tipo del contenido del arreglo.

¿Es posible lo que estoy preguntando? ¿Se puede filtrar extends keyof solo a las claves que son tipos de array, y se pueden asignar los tipos del contenido de esos arrays a otro parámetro de tipo?

Gracias de antemano

Editar: Entonces, he logrado obtener el tipo del arreglo usando como clave la variable val. Así que lo siguiente:

export const mutateConcat = <T, U, K extends keyof T>(state: T, key: K, val: T[K] & U[], arrayDistinctProperty?: keyof U) => {
  // U now contains the array type, and `arrayDistinctProperty` is typed to a property on that array type object
  ((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}

Sin embargo, esto no resuelve la fealdad del casting, porque TypeScript no sabe con certeza que T[K] es un tipo de arreglo. ¿Es posible indicarle que solo permita que mi parámetro key sea un tipo de arreglo en T?

Respuesta #1

La otra respuesta es correcta, pero quería añadir que puedes hacer que funcione si haces que state sea un tipo relativamente concreto Record<K, U[]>, en lugar del tipo más genérico T extends Record<K, U[]>. Esto ayuda al compilador a ver la asignación como segura, ya que T extends {a: string[]} podría ser {a: Array<"lit">}, y entonces obj.a.push("oops") sería un error:

const mutateConcat = <K extends keyof any, U>(
  state: Record<K, U[]>,
  key: K,
  val: U[]
) => {
  state[key] = state[key].concat(val); // okay
};

El único problema que veo allí es que si pasas literales de objeto en el parámetro state cuando llamas a mutateConcat(), obtendrás presumiblemente una verificación de propiedades excesivas no deseada en propiedades que no son arrays:

mutateConcat({ a: [1, 2, 3], b: 1 }, "a", [4, 5, 6]); // excess property checking 😢

Y cualquier cosa que haga con la firma de mutateConcat() para abordar esto hace que el compilador se confunda demasiado dentro de la implementación y vuelves a convertir afirmar. Si aún quieres seguir por este camino, puedes evitar la comprobación de propiedades excesivas al no usar literales de objetos nuevos, como en:

const obj = { a: [1, 2, 3], b: 1 };
mutateConcat(obj, "a", [4, 5, 6]); // okay

Si esto satisface tu caso de uso depende de ti. ¡Está bien, buena suerte!

Enlace al código

Respuesta #2

Enfoque #1

/**
 * Push into the existing array.
 */
export const mutateConcat1 = <
  TState extends Record<TKey, unknown[]>,
  TKey extends string,
  >(
    state: TState,
    key: TKey,
    newArray: TState[TKey]
  ) => {
  state[key].push(...newArray);
};

Enfoque #2

El casteo con as aquí podría ser inevitable. Sin él recibimos, Type 'unknown[]' is not assignable to type 'TState[TKey]'. Esto se debe a que le dijimos al compilador que el arreglo es de tipo unknown[] en el Record. Intenté inferir el tipo del arreglo pero no pude hacerlo funcionar. Sin embargo, el casteo parece muy seguro, dado que sabemos con certeza que tanto state[key] como newArray son del mismo tipo. Si tan solo supiera cómo explicarle eso al compilador.

/**
 * Overwrite the original array with concat.
 */
export const mutateConcat2 = <
  TState extends Record<TKey, unknown[]>,
  TKey extends string,
  >(
    state: TState,
    key: TKey,
    newArray: TState[TKey]
  ) => {
  state[key] = state[key].concat(newArray) as TState[TKey];
};

Pruebas

/**
 * Test Cases
 */

const goodState = { array1: [1, 2, 3] };
const badState = { array1: new Date() };

// ok
mutateConcat1(goodState, 'array1', [4, 5, 6]);
mutateConcat2(goodState, 'array1', [4, 5, 6]);

// errors
mutateConcat1(goodState, 'array1', ['4', 5, 6]);
mutateConcat1(goodState, 'array2', [4, 5, 6]);
mutateConcat1(badState, 'array1', [4, 5, 6]);

mutateConcat2(goodState, 'array1', ['4', 5, 6]);
mutateConcat2(goodState, 'array2', [4, 5, 6]);
mutateConcat2(badState, 'array1', [4, 5, 6]);

Enlace al código.