Búsqueda de sitios web

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;