Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to infer typed mapValues using lookups in typescript?

Similar to:

How to infer a typed array from a dynamic key array in typescript?

I'm looking to type a generic object which receives a map of arbitrary keys to lookup values, and returns the same keys with typed values (like a typed _.mapValues).

The ability to get a singular typed property from an object is documented and works. With arrays, you need to hardcode overloads to typed tuples, but for objects i'm getting a 'Duplicate string index signature' error.

export interface IPerson {
    age: number;
    name: string;
}

const person: IPerson = {
    age: 1,
    name: ""
}

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name];
}

const a = getProperty(person, 'age');
// a: number

const n = getProperty(person, 'name');
// n: string

function getProperties<T, K extends keyof T>(obj: T, keys: { [key: string]: K }) {
    const def: { [key: string]: T[K] } = {};
    return Object.entries(keys).reduce((result, [key, value]: [string, K]) => {
        result[key] = getProperty(obj, value);
        return result;
    }, def);
}

const { a2, n2 } = getProperties(person, {
    a2: 'name',
    n2: 'age'
});

// Result:
// {
//     a2: string | number, 
//     n2: string | number
// }

// What I'm looking for:
// {
//     a2: string, 
//     n2: number' 
// }

How can this be implemented with typescript?

like image 513
blugavere Avatar asked Mar 06 '23 00:03

blugavere


1 Answers

As long as it's working at runtime, you can tell TypeScript how to rename keys using mapped types:

type RenameKeys<T, KS extends Record<keyof KS, keyof T>> = {[K in keyof KS]: T[KS[K]]};

function getProperties<T, KS extends Record<keyof KS, keyof T>>(
    obj: T,
    keys: KS
): RenameKeys<T, KS> {
    const def = {} as RenameKeys<T, KS>;
    return (Object.entries(keys) as Array<[keyof KS, any]>)
      .reduce((result, [key, value]) => {
        result[key] = getProperty(obj, value);
        return result;
    }, def);
}

This should behave as you expect in the type system. Highlights: the type of keys is given a type parameter named KS, which is constrained to be Record<keyof KS, keyof T>, which more or less means "I don't care what the keys are, but the property types need to be the keys from T". Then, the RenameKeys<T, KS> walks through the keys of KS and plucks property types out of T related to them.

Finally, I needed to do a few type assertions... def is RenameKeys<T, KS>. The type of the value in [key, value] I just made any, since it's hard for the type system to verify that result[key] will be the right type. So it's a bit of a fudging of the implementation type safety... but the caller of getProperties() should be happy:

const {a2, n2} = getProperties(person, {
  a2: 'name',
  n2: 'age'
});
// a2 is string, n2 is number.

Playground link to code

like image 82
jcalz Avatar answered Mar 09 '23 01:03

jcalz