Search code examples
typescriptkeyrecord

How to build a typescript function that returns an record with the given keys from another object


I need to build a function that takes 0 or more keys from a given object and returns a record with those given keys only.

I know, however, how to request a parameter that is an array of keys from an object:

function myFunc<T>(field: (keyof T)[]): void {}

Problem is that I'm not sure how to make it so that instead of returning void, it returns a record with the given fields as keys:

function myFunc<T, K extends (keyof T)[]>(fields: K): Record<K, number> {}

This yields Type 'K' does not satisfy the constraint 'string | number | symbol'. which is fair.

To make it clearer, this would be the implementation of this function in javascript:

function myFunc(fields) {
   return fields.reduce((result, key, idx) => ({...result, [key]: idx}), {});
}

And this would be an example of the input and expected output for that function:

type Person = {
  name: string,
  age: number,
  citizen: boolean
}

console.log(myFunc<Person>(['name', 'citizen'])); // Object { name: 0, citizen: 1 }
console.log(myFunc<Person>(['name', 'years'])); // Error: Argument of type '"years"' is not assignable to parameter of type '"name" | "age" | "citizen"'

It looks like it should be a very trivial thing that I'm missing completely but I can't find the solution even by searching on internet 😔

Thanks in advance!


Solution

  • Consider this example:

    type Person = {
        name: string,
        age: number,
        citizen: boolean
    }
    
    
    type Result<Fields extends PropertyKey[]> = {
        [Prop in keyof Fields as Fields[Prop] extends Fields[number] ? Fields[Prop] : never]: Exclude<Prop, number>
    }
    
    function withObj<Obj,>(obj: Obj): <Field extends keyof Obj, Fields extends Field[]>(fields: [...Fields]) => Result<Fields>
    function withObj<Obj,>(obj: Obj) {
        return (fields: string[]) =>
            fields.reduce((result, key, idx) => ({ ...result, [key]: idx }), {});
    }
    
    const myFunc = withObj({
        name: 'John',
        age: 42,
        citizen: true
    })
    
    const x = myFunc(['name', 'citizen']) // ok
    x.name // 0
    const y = myFunc(['name', 'years']) // error
    

    Playground

    Result iterates through each array prop and check whether Fields[index] is a subtype of array elements. If yes - rename array key to appropriate array element and use index in a place of a value.

    Also, I have added curried version of myFunc just for the proper inference.