typescript - ¿Hay una manera más limpia de actualizar el valor de **keyof T** sobre la marcha?
Estoy tratando de copiar los valores de las propiedades de un S fuente a un T objetivo. Después de comprobar programáticamente que el campo existe, quiero que TS me permita acceder a él, pero no lo hace:
● Test suite failed to run
src/util/Copy.ts:18:18 - error TS2536: Type 'keyof T' cannot be used to index type 'S'.
18 return source[field] === target[field];
~~~~~~~~~~~~~
src/util/Copy.ts:33:11 - error TS2536: Type 'keyof S' cannot be used to index type 'T'.
33 match[field] = source[field];
~~~~~~~~~~~~
Podría hacer que source se extendiera de target para aceptar únicamente los campos que están en ambos tipos. Pero mi método de utilidad debería manejar incluso tipos completamente diferentes. Aquí está el código que falla
export function copyFieldsWhenMatching<S extends object, T extends object>(
sourceArray: S[],
targetArray: T[],
equalFields: (keyof T)[],
fieldsToCopy: (keyof S)[],
allowMultipleOccurrences: boolean
): T[] {
const toReturn = [...targetArray];
for (const source of sourceArray) {
// Find matches based on equalFields
const matches = toReturn.filter((target) => {
return equalFields.every((field) => {
// eslint-disable-next-line no-prototype-builtins
if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) {
return source[field] === target[field];
}
return true; // Property doesn't exist in either source or target; continue matching.
});
});
if (!allowMultipleOccurrences && matches.length > 1) {
throw new DuplicateMatchesError(
"More than one match found for the source object.",
matches
);
} else if (matches.length >= 1 || allowMultipleOccurrences) {
// Copy fields from source to matches
for (const match of matches) {
for (const field of fieldsToCopy) {
match[field] = source[field];
}
}
}
}
return toReturn;
}
Y así es como lo hackeé para pasar (convirtiendo el objetivo en la fuente y viceversa solo para poder pasar). Así que eso me llevó a la pregunta en el título:
¿Hay una manera más limpia de actualizar el valor de keyof T sobre la marcha?
import { DuplicateMatchesError } from "../errors";
export function copyFieldsWhenMatching<S extends object, T extends object>(
sourceArray: S[],
targetArray: T[],
equalFields: (keyof T)[],
fieldsToCopy: (keyof S)[],
allowMultipleOccurrences: boolean,
): T[] {
const toReturn = [...targetArray];
for (const source of sourceArray) {
// Find matches based on equalFields
const matches = toReturn.filter((target) => {
return equalFields.every((field) => {
// eslint-disable-next-line no-prototype-builtins
if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) {
return (
// dirty assertion to hack TS
(source as unknown as T)[field] ===
target[field]
);
}
return true; // Property doesn't exist in either source or target; continue matching.
});
});
if (!allowMultipleOccurrences && matches.length > 1) {
throw new DuplicateMatchesError(
"More than one match found for the source object.",
matches,
);
} else if (matches.length >= 1 || allowMultipleOccurrences) {
// Copy fields from source to matches
for (const match of matches) {
for (const field of fieldsToCopy) {
// dirty assertion to hack TS
(match as unknown as S)[field] = source[field];
}
}
}
}
return toReturn;
}
Respuesta #1
if (source.hasOwnProperty(field)) {
return source[field] === target[field];
}
En este código de tu ejemplo, estás usando hasOwnProperty() para verificar si source tiene la propiedad. Sin embargo, hasOwnProperty() no restringe su objetivo, por lo que TS no restringe source para incluir la propiedad especificada por field.
Para solucionar esto, puedes crear un guardián de tipo definido por el usuario que llame a hasOwnProperty() y limite el objeto objetivo.
TypeScript reduce los tipos de las variables en algunos casos, como al usar typeof. Al usar un tipo de retorno llamado predicado de tipo, podemos crear funciones que reducen el tipo de su entrada. A esto se les llama protectores de tipo definidos por el usuario.
Aquí hay uno para hasOwnProperty():
function hasOwnProperty<O extends object, K extends PropertyKey>(
obj: O,
key: K,
): obj is O & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(obj, key)
}
Entonces puedes usarlo así:
if (hasOwnProperty(source, field)) {
return source[field] === target[field]
}
Nota: Esto está "destinado" a ser usado con claves de cadena. Dado que field se tipa como keyof T, en realidad tipa source como siendo de tipo S & Record<keyof T, unknown> (es decir, teniendo todas las propiedades de T). Sin embargo, dado que simplemente se usa la misma variable (field) para indexar source, está bien. Pero ten cuidado si haces más.
Aquí está eso incluido en tu código: TypeScript Playground.
Para más información sobre los type guards, están la documentación de TS y el excelente TypeScript Deep Dive de basarat.
Respuesta #2
Recomendaría convertir todos los arrays a (S & T)[]
export function copyFieldsWhenMatching<S extends object, T extends object>(
sourceArray: S[],
targetArray: T[],
equalFields: (keyof T)[],
fieldsToCopy: (keyof S)[],
allowMultipleOccurrences: boolean,
): T[] {
const toReturn = [...targetArray] as (S & T)[];
for (const source of (sourceArray as (S & T)[])) {
// Find matches based on equalFields
const matches = toReturn.filter((target) => {
return equalFields.every((field) => {
if (Object.hasOwn(source, field) && Object.hasOwn(target, field)) {
return source[field] === target[field]
}
return true; // Property doesn't exist in either source or target; continue matching.
});
});
if (!allowMultipleOccurrences && matches.length > 1) {
throw new Error(
"More than one match found for the source object.",
{ cause: matches },
);
}
if (matches.length === 1 || matches.length > 1 && allowMultipleOccurrences) {
// Copy fields from source to matches
for (const match of matches) {
for (const field of fieldsToCopy) {
match[field] = source[field];
}
}
}
}
return toReturn;
}
