Home > Blockchain >  Typescript - Type narrowing when looping on keys of object
Typescript - Type narrowing when looping on keys of object

Time:07-07

When I have to iterate on the keys of mappedTypes, I always struggle to make Typescript narrow the type of the value associated with the key I am iterating on and always finish with a ts-ignore ...

A code sample demonstrating the issue worths a 1000 words :) Here it is !

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 {}

What can I do to narrow the type of the value ? If it is not possible, what pattern would you use in this case ?

Thanks a lot for your help ! This problem is haunting me !

CodePudding user response:

I have added one more type Updatetable<Type> and another function update to make assignment type safe. I hope this works for you:

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 {};

CodePudding user response:

Credits to @okan aslan for pointing me on the right direction. Thanks again ! Here is a modified version of the code sample that solves the problem.

Look at the introduction of the two helper functions update and 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 {}
  • Related