TypeScript: Genéricos: ¿Hay una manera segura y estricta de elegir propiedades? (que deben existir tanto en el tipo origen como en el tipo destino)
Quiero transformar el tipo A en tipo B, pero quiero recibir advertencias si elijo propiedades que no están definidas en ambos tipos.
type Source = {
id: number;
foo: string;
onlyOnSource: string;
}
type Target = {
id: number;
foo: string;
onlyOnTarget?: string;
}
const source: Source = {
id: 1,
foo: 'hello',
onlyOnSource: 'test',
}
const transformer1 = (response?: Source): Partial<Target> => {
return {
id: response?.id,
foo: response?.foo,
// wrong: undefined, // cool. error is catched: Object literal may only specify known properties, and 'wrong' does not exist in type 'Partial<Target>'
// onlyOnTarget: response?.onlyOnTarget, // cool. error is catched: Property 'onlyOnTarget' does not exist on type 'Source'
};
};
...en general, esto funciona mejor. Pero tengo que escribir todas las propiedades dos veces (id, foo). Y es posible asignarlas incorrectamente (es decir, id: response?.foo)
Así que probé una función genérica «safePick» (que es similar a lodash/pick, pero advierte cuando las propiedades no existen)
const safePick = <T, K extends keyof T>(source?: T, ...keys: K[]): Partial<Pick<T, K>> => {
if (!source) return {};
const target: Partial<Pick<T, K>> = {};
keys.forEach(key => {
if (source[key] !== undefined) target[key] = source[key];
});
return target;
};
const transformer2 = (response?: Source): Partial<Target> => {
// return safePick(response, 'id', 'foo', 'wrong'); // cool. error is catched: Argument of type '"wrong"' is not assignable to parameter of type 'keyof Source'
return safePick(response, 'id', 'foo', 'onlyOnSource'); // WRONG! 'onlyOnSource' should not be alowed on type Target!
};
Pero eso no es quejarse cuando asigno la propiedad "onlyOnSource" al tipo Target. :(
Incluso cuando quito la parte de "Parcial", todavía no le importa.
Pero quiero que esto falle con una advertencia. Idealmente de una manera genérica, para no tener que pasar todas las claves de ambos tipos manualmente.
Probé muchas cosas. Incluso usar "zod". Pero no pude encontrar una buena solución. ¿Alguien tiene alguna idea?
Respuesta #1
También tienes que poner el tipo Target en la función como argumento. Esto se debe a que un objeto con campos adicionales será un subtipo del tipo de retorno de Patrial<Target>, por lo que no fallará como quieres.
Luego usa ese tipo para calcular algún tipo de retorno incorrecto si hay un campo extra.
const safePick = <Target>() => {
return <T, K extends keyof T>(source?: T, ...keys: K[]): TargetType<Target, K, T> => {
// @ts-ignore
if (!source) return {};
const target: Partial<Pick<T, K>> = {};
keys.forEach(key => {
if (source[key] !== undefined) target[key] = source[key];
});
// @ts-ignore
return target;
};
}
La función a nivel de tipo que calculará un tipo incorrecto es esta:
type TargetType<Target, K extends keyof T, T> = Exclude<K, keyof Target> extends never ? Partial<Pick<T, K>> : void
Aquí, verificas cuál es el resultado si tomas todas las claves de Source y excluyes de ellas todas las claves de Target. Si hay never, eso significa que no hay campos adicionales en Source, y puedes devolver de manera segura tu tipo deseado. De lo contrario, devuelve el tipo void "fail", lo que provocará un error de compilación.
const transformer2 = (response?: Source): Partial<Target> => {
return safePick<Target>()(response, 'id', 'onlyOnSource'); // WRONG! 'onlyOnSource' should not be alowed on type Target!
};
Esto resultará entonces en esto:
Type 'void' is not assignable to type 'Partial<Target>'.ts(2322)
