TypeScript - Cómo combinar tipos de índice con tipos de intersección
Estoy intentando definir un tipo que acepte propiedades residuales de la siguiente manera:
export type Base = {
numberProperty: number;
booleanProperty: boolean;
};
export type Residual = {
[key: string]: string;
};
export type Complete = Base & Residual;
const abc: Complete = {
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
};
En otras palabras, quiero asegurarme de que numberProperty y booleanProperty siempre sean de tipo number y boolean respectivamente, pero cualquier otra propiedad debería ser string. Sin embargo, al compilar esto (3.9.2), obtengo el siguiente error:
error TS2322: Type '{ numberProperty: number; booleanProperty: true; residualProperty: string; }' is not assignable to type 'Complete'.
Type '{ numberProperty: number; booleanProperty: true; residualProperty: string; }' is not assignable to type 'Residual'.
Property 'numberProperty' is incompatible with index signature.
Type 'number' is not assignable to type 'string'.
13 const abc: Complete = {
~~~
Found 1 error.
He encontrado preguntas similares y documentación que aborda problemas similares, pero aún no he encontrado una respuesta concluyente sobre cómo hacer esto.
Respuesta #1
El problema es que Residual tiene un conflicto con las claves numberProperty y booleanProperty de Base, porque no son cadenas.
Para arreglarlo necesitas cambiar Residual para que diga que todo excepto Base es cadena. Después de eso, Residual respeta a Base y se pueden combinar juntos.
export type Base = {
numberProperty: number;
booleanProperty: boolean;
};
export type Residual = {
[K in keyof any]: K extends keyof Base ? never : string;
};
export type Complete = Base | Residual;
const abc: Complete = {
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
};
Si no puedes cambiar Residual, entonces deberías reconsiderar tu código para evitar su unión porque tiene un conflicto y no será válido.
const val1: Residual = {
numberProperty: 'string', // valid, positive
};
const val2: Base = {
numberProperty: 'string', // invalid, negative
};
const val3: Residual | Base = {
numberProperty: 'string', // positive & negative = negative.
};
Para un respeto profundo del tipo, debemos omitir string y siempre operar con claves definidas. Usualmente, las personas tienen 2 opciones: especificar todas las claves o usar una función de validación.
export type Base = {
numberProperty: number;
booleanProperty: boolean;
};
export type Residual<KEYS extends keyof any = keyof any> = {
[K in KEYS]: string;
};
export type Complete<K extends keyof any> = Base & Residual<Exclude<K, keyof Base>>;
const abc: Complete<'residualProperty'> = {
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
};
const booleanVariable: boolean = abc.booleanProperty;
const validateComplete = <T extends Complete<K>, K extends keyof T>(value: T): Complete<K> => value;
const abc2 = validateComplete({
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
});
const booleanVariable2: boolean = abc2.booleanProperty;
const stringVariable2: string = abc2.residualProperty;
