Home > Back-end >  Typescript - use object literal to pick/index other type
Typescript - use object literal to pick/index other type

Time:11-09

Say I have an interface

interface Person {
    id: string;
    name: string;
    dob: Date;
    dod?: Date;
    nicknames?: string[];
    age: number;
    height: number;
    weight: number;
}

And I want a generic function that can take a type like this and map it back to a new type with just implicit types. It also has to be able to rename fields as-needed. e.g.

const a = getThing<Person>({
    id: "id" as const, // The const lets TS treat it as a literal and not just any `string`
    name: "name" as const,
    dateOfBirth: "dob" as const,
});

a would have type

{
    id: string;
    name: string;
    dateOfBirth: Date;
}

This might not be possible, but I know there's plenty of smart people on here. All of my attempts get stuck when trying to index the generic param (Person) with the implicit keys/values of the input param. e.g.

function getThing<
    T,
    R = { [k: string]: keyof T }
>(
    fields: R
): { [k in keyof R]: T[R[k]] } {
    ... // implementation unimportant?
}

Even if you clearly specify that the values of R are all keys of T, you get an error in the return value that R[k] cannot be used to index T.

I know there are a lot of powerful aspects of TS, but sometimes simple logical stuff seems beyond reach. I'm on a slightly older version of TS, 4.3.5, and might be able to upgrade if necessary.

EDIT

Is the current approach easily modifiable to allow for nested fields? e.g. if Person also had a contact field:

interface ContactInfo {
    homePhone: string;
    mobilePhone: string;
    email: string;
    address: string;
}

interface Person {
    ...
    contact: ContactInfo;
    ...
}

And we wanted to map type and map those subfields as well (and so on, down to many levels), where I could get resulting type:

{
    id: string;
    name: string;
    dateOfBirth: Date;
    ice: { // renamed from `contact`
        phone: string; // renamed from `homePhone`
    }
}

Since you're probably wondering the reason for these crazy constraints, I'm trying build a self-typing GraphQL query tool. I have the TS types for the available objects and their fields, and want to implicitly create the correct output type while also supporting the GQL field renaming behavior. There's of course lots of other GQL features but 99% of the time I just need the basic type.

CodePudding user response:

Since partial type inference is not in TypeScript, you would need to use currying here:

function getThing<T>() {
    return function<R extends Record<keyof any, keyof T>>(remapped: R): {
        [K in keyof R]: T[R[K]]
    } { return null! }
}

In the return type, you'd map over the R and change the type to T[R[K]] since R[K] is a key of T. Then you'd be able to use it like this:

// no 'as const' needed!
const result = getThing<Person>()({
    id: "id",
    name: "name",
    dateOfBirth: "dob",
});

Playground

  • Related