node.js - TypeScript: interfaz genérica como unión de otras interfaces
Me gustaría crear una interfaz genérica con propiedades que representen una unión de propiedades de otras interfaces.
Digamos que tengo dos interfaces
interface A {
something: string;
somethingElse: number;
}
interface B {
something: Array<string>;
}
No quiero escribir la interfaz C como
interface C {
something: string | Array<string>;
somethingElse?: number;
}
porque eso significaría que cada vez que modifique cualquiera de las interfaces A o B, tendría que modificar manualmente también la interfaz C.
Por lo que he visto en la documentación de TypeScript así como en las respuestas aquí en Stack Overflow, debería declarar un nuevo tipo
type unionOfKeys = keyof A | keyof B;
e implementar formulario de interfaz genérica
interface GenericInterface {
<T>(arg: T): T;
}
Estaba pensando en la dirección de
interface C {
<T extends unionOfKeys>(arg: T): T extends unionOfKeys ? A[T] | B[T] : any
}
pero eso falla debido a una discrepancia entre el número de propiedades y sus tipos.
Agradecería cualquier tipo de ayuda. Gracias.
Respuesta #1
Creo que la siguiente versión de MergeUnion<T> podría comportarse como deseas:
type MergeUnion<T> = (
keyof T extends infer K ? [K] extends [keyof T] ? Pick<T, K> & {
[P in Exclude<(T extends any ? keyof T : never), K>]?:
T extends Partial<Record<P, infer V>> ? V : never
} : never : never
) extends infer U ? { [K in keyof U]: U[K] } : never;
type C = MergeUnion<A | B>;
// type C = {
// something: string | string[];
// somethingElse?: number | undefined; }
// }
Esto es similar a la otra respuesta en que encuentra la unión de todas las claves de todos los componentes de T (llámalo UnionKeys, definido como T extends any ? keyof T : never) y devuelve un tipo mapeado con todas ellas. La diferencia es que aquí también encontramos la intersección de todas las claves de todos los componentes de T (llámalo IntersectKeys, definido simplemente como keyof T) y dividimos las claves de T en dos conjuntos de claves. Las de la intersección están presentes en cada componente, por lo que simplemente podemos hacer Pick<T, IntersectKeys> para obtener las propiedades comunes. El resto, Exclude<UnionKeys, IntersectKeys>, será opcional en el tipo final.
ACTUALIZACIÓN 2019-08-23: el error mencionado más abajo parece estar solucionado desde TS3.5.1
Es bastante feo, y lo limpiaría si me sintiera mejor al respecto. El problema es que todavía hay un problema cuando alguna de las propiedades que aparecen en todos los constituyentes es opcional. Hay un error en TypeScript (a partir de TS3.5) donde en {a?: string} | {a?: number}, la propiedad a se ve como una propiedad requerida como {a: string | number | undefined}, mientras que sería más correcto tratarla como opcional si alguno de los constituyentes la tiene como opcional. Ese error se filtra en MergeUnion:
type Oops = MergeUnion<{a?: string} | {a?: number}>
// type Oops = { a: string | number | undefined; }
No tengo una gran respuesta allí que no sea aún más complicada, así que me detendré aquí.
Tal vez esto sea suficiente para tus necesidades. O tal vez la respuesta de @TitianCernicova-Dragomir sea suficiente para tus necesidades. Espero que estas respuestas te ayuden; ¡buena suerte!
Enlace al código
Respuesta #2
Ni los tipos de intersección ni los tipos de unión nos llevarán a C. Un tipo de unión (A | B) solo permitirá el acceso a las propiedades comunes. Una intersección (A & B) permitirá el acceso a todas las propiedades, pero si las propiedades difieren entre A y B, la propiedad será una intersección de las dos propiedades (por ejemplo, something será string & Array<string>; lo cual no es muy útil aquí).
La solución es construir un tipo mapeado personalizado que tomará claves de todos los tipos pasados y creará una unión de tipos de propiedades de cada miembro:
interface A {
something: string;
somethingElse: number;
}
interface B {
something: Array<string>;
}
type KeyOf<T> = T extends any ? keyof T : never;
type PropValue<T, K extends PropertyKey> = T extends Record<K, infer V> ? V : never;
type Merge<T> = {
[P in KeyOf<T>] : PropValue<T, P>
}
type C = Merge<A | B>
// type C = {
// something: string | string[];
// somethingElse: number;
// }
KeyOf tomará un T y, si T es una unión, devolverá las claves de todos los miembros de la unión. Hace esto usando la propiedad distributiva de los tipos condicionales.
type K = KeyOf<{a : number} | { b: number }> // "a" | "b".
Esto es necesario ya que keyof para una unión solo devolverá los miembros comunes. (keyof ({a : number} | { b: number }) es never).
PropValue también utiliza la propiedad distributiva de los tipos condicionales para extraer una unión de todos los tipos de valores de una clave.
type V = PropValue<{a : number} | {a : string} |{ b: number }, 'a'> // string | number
Al ponerlo junto en un tipo mapeado, obtenemos Merge que mapea sobre todas las claves en cada miembro de la unión y mapea a una unión de todos los posibles tipos de propiedades.
