Robert
Robert

Reputation: 54

Typescript - How to use generics to define a function that returns indexable properties of an object?

I am having trouble writing the correct types for a simple function.

I want to create a function that when given a key (keyof any) or callback will return another function. This function takes in some data and returns another key.

In other words both toKey('propertyName') and toKey(value => value.propertyName) would return the function value => value.propertyName. When called, the function would return the given value of said property only if the value is a string, number or symbol type.

Here's an example:

function toKey(keyIdentity) { /* snippet */ }

interface IPerson {
    firstName: string,
    lastName: string,
    age: number,
    emails: string[]
};

const getKey1 = toKey((person: IPerson) => person.emails[0]);
const getKey2 = toKey('firstName');
const getKey3 = toKey('emails');

const person: IPerson = {
    firstName: 'Craig',
    lastName: 'Lipton',
    age: 46,
    emails: [
        '<email-1>',
        '<email-2>'
    ]
};

getKey1(person); // returns "<email-1>";
getKey2(person); // returns "Craig";
getKey3(person); // not allowed;

I've attempted to use generics and overloads to achieve the correct typing but it gets complicated very quickly.

function toKey(key?: null): () => void;
function toKey<K extends keyof any>(key: K): <U extends keyof any>(item: Record<K, U>) => U;
function toKey<T extends (...args: any[]) => keyof any>(key: T): T;
function toKey(key: any): any {
    if (isNil(key)) {
        return noop;
    }

    return isFunction(key) ? key : (value: T) => value[key];
}

Is there any simpler way to write this?

Upvotes: 2

Views: 1368

Answers (2)

ford04
ford04

Reputation: 74500

If you want to be explicit in your types, the built-in PropertyKey type can be used:

// type PropertyKey = string | number | symbol

function toKey<K extends PropertyKey>(key: K): 
  <T extends Record<K, PropertyKey>>(t: T) => PropertyKey
function toKey<T>(fn: (t: T) => PropertyKey): (t: T) => PropertyKey
function toKey(key: PropertyKey | ((t: Record<PropertyKey, PropertyKey>) => PropertyKey)) {...}

Test it:

toKey((person: IPerson) => person.emails[0])(person); // "<email-1>";
toKey('firstName')(person); // "Craig";
toKey('emails')(person); // error, emails prop is not string | number | symbol
toKey('lastName')({ firstName: "Lui" }); // error, object passed in has no key "lastName"

Code sample

Upvotes: 1

Akash Kava
Akash Kava

Reputation: 39916

You have to use this,

function toKey<T, K extends keyof T = keyof T>(key: K): (v: T) => T[K];
const getKey3 = toKey<IPerson>('emails');
const getKey3 = toKey<IPerson,"emails">('emails');

Example

Upvotes: 0

Related Questions