2

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 !

1

2 Answers 2

2

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 {};
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a lot for your answer ! Very much helpful ! It insipired me to also do something similar with the transformers call
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.