Búsqueda de sitios web

Typescript - Acotamiento de tipos al iterar sobre las claves de un objeto


Cuando tengo que iterar sobre las claves de mappedTypes, siempre me cuesta hacer que Typescript reduzca el tipo del valor asociado con la clave sobre la que estoy iterando y siempre termino con un ts-ignore ...

¡Un ejemplo de código que demuestra el problema vale más que mil palabras :) Aquí está!

type Person = {
  firstName?: string
  lastName?: string
  age?: number
}

type Update<T> = {
  before: T,
  after: T,
}

type Updates = { [Key in keyof Person]: Update<Person[Key]> }

/**
 * Dummy transformers for illustration purposes
 */
const transformers : {[Key in keyof Person]: (value: Person[Key]) => Person[Key]} = {
  firstName: value => value?.toUpperCase(),
  lastName: value => value?.toUpperCase(),
  age: value => typeof value === 'undefined'? 0 : value + 2
}

/**
 * Input: {firstName: 'david', age: 23}
 * Output: {firstName: {before: 'david', after: 'David'}, age: {before: 22, after: 24}}
 * @param person
 */
function enrichPerson (person: Person): Updates {
  return Object.keys(person).reduce(
    (previousUpdates, key) => {
      const typedKey = key as keyof Person
      const result = { ...previousUpdates }

      result[typedKey] = {
        before: person[typedKey],                           // <--- Typing problem Here
        after: transformers[typedKey](person[typedKey]),    // <--- Typing problem Here
      }

      return result
    },
    {} as Updates
  )
}

export {}

¿Qué puedo hacer para restringir el tipo del valor? Si no es posible, ¿qué patrón usarías en este caso?

¡Muchas gracias por tu ayuda! ¡Este problema me está atormentando!

Respuesta #1

He añadido un tipo más Updatetable<Type> y otra función update para hacer que el tipo de asignación sea seguro. Espero que esto funcione para ti:

type Person = {
    firstName?: string;
    lastName?: string;
    age?: number;
};

type Update<T> = {
    before: T;
    after: T;
};

type Updatetable<Type> = { [Key in keyof Type]: Update<Type[Key]> };
type Updates = Updatetable<Person>;

/**
 * Dummy transformers for illustration purposes
 */
const transformers: { [Key in keyof Person]: (value?: Person[Key]) => Person[Key] } = {
    firstName: (value) => value?.toUpperCase(),
    lastName: (value) => value?.toUpperCase(),
    age: (value) => (typeof value === "undefined" ? 0 : value + 2),
};

/**
 * Input: {firstName: 'david', age: 23}
 * Output: {firstName: {before: 'david', after: 'David'}, age: {before: 22, after: 24}}
 * @param person
 */
function enrichPerson(person: Person): Updates {
    return Object.keys(person).reduce<Updates>((previousUpdates, key) => {
        const typedKey = key as keyof Person;
        const result: Updates = { ...previousUpdates };

        update(result, typedKey, { before: person[typedKey], after: person[typedKey] });

        return result;
    }, {});
}

function update<Type, Key extends keyof Type>(result: Updatetable<Type>, key: Key, value: Update<Type[Key]>): void {
    result[key] = value;
}

export {};

Respuesta #2

Créditos a @okan aslan por señalarme la dirección correcta. ¡Gracias de nuevo! Aquí hay una versión modificada del ejemplo de código que resuelve el problema.

Observa la introducción de las dos funciones auxiliares update y applyTransformer.

type Person = {
  firstName?: string;
  lastName?: string;
  age?: number;
};

type Update<T> = {
  before: T;
  after: T;
};

type Updatetable<Type> = { [Key in keyof Type]: Update<Type[Key]> };
type Updates = Updatetable<Person>;

type Transformer<Type, Key extends keyof Required<Type>> = (x: Type[Key]) => Type[Key]
type Transformers<Type> = { [Key in keyof Required<Type>]: Transformer<Type, Key> }


function update<T, K extends keyof T> (obj: Updatetable<T>, key: K, newValue: Update<T[K]>): void {
  obj[key] = newValue
}

function applyTransformer<T, K extends keyof Required<T>> (
  transformers: Transformers<T>,
  key: K,
  input: T[K]
): T[K] {
  return transformers[key](input)
}


/**
 * Dummy transformers for illustration purposes
 */
const transformers: { [Key in keyof Required<Person>]: (value?: Person[Key]) => Person[Key] } = {
  firstName: (value) => value?.toUpperCase(),
  lastName: (value) => value?.toUpperCase(),
  age: (value) => (typeof value === 'undefined' ? 0 : value + 2),
}

/**
 * Input: {firstName: 'david', age: 22}
 * Output: {firstName: {before: 'david', after: 'David'}, age: {before: 22, after: 24}}
 * @param person
 */
function enrichPerson (person: Person): Updates {
  return Object.keys(person).reduce<Updates>((previousUpdates, key) => {
    const typedKey = key as keyof Person
    const result: Updates = { ...previousUpdates }

    update(result, typedKey, {
      before: person[typedKey],
      after:  applyTransformer(transformers, typedKey, person[typedKey])
    })

    return result
  }, {})
}

export {}