Home > Enterprise >  How to get specific keys from interface using template literal types?
How to get specific keys from interface using template literal types?

Time:10-18

I have the following code that takes in a string array of possibleColumns for a given database table. It then uses the incoming source and tries to match it to a column that contain {source}_id.

e.g. the possibleColumns could contain ["id", "name", "duedil_id", "salesforce_id"] and the source is duedil it then checks for any columns called duedil_id and returns it.

The logic is fine but the returned types are not correct, currently it just returns the possible value as any of the columns in that type but I want to use template literal types to just return any values that contain <value>_id. As seen in the comments I'm trying to use the Extract type but no luck.

/**
 * Takes a list of columns for a specific table and a source and checks for any columns that
 * match `${source}_id`.
 * @template T The desired output type
 * @param possibleColumns string array of columns of type T
 * @param source The source to convert in question
 * @returns a key of one of the columns of T or undefined if not matched
 */
export function getProviderIdColumn<T>(possibleColumns: (keyof T)[], source: string) {
    // Attempt to extract only keys that contain _id at the end
    // as Extract<keyof T extends `${infer _}_id` ? keyof T : never, T>
    const providerIdColumn = `${source}_id` as keyof T;
    if (possibleColumns.includes(providerIdColumn)) {
        return providerIdColumn;
    }

Current output

"id" | "name" | "duedil_id" | "salesforce_id"

Desired output

"duedil_id" | "salesforce_id"

My knowledge of typescript isn't great so please disregard any misused terminology.

Minimal working example

export interface Model {
    id: number,
    name: string | null,
    boardex_id: string | null,
    source: string,
    duedil_id: string | null,
    zoom_info_id: string | null,
}

/**
 * Takes a list of columns for a specific table and a source and checks for any columns that
 * match `${source}_id`.
 * @template T The desired output type
 * @param possibleColumns string array of columns of type T
 * @param source The source to convert in question
 * @returns a key of one of the columns of T or undefined if not matched
 */
export function getProviderIdColumn<T>(possibleColumns: (keyof T)[], source: string) {
    // Attempt to extract only keys that contain _id at the end
    // as Extract<keyof T extends `${infer _}_id` ? keyof T : never, T>
    const providerIdColumn = `${source}_id` as keyof T;
    if (possibleColumns.includes(providerIdColumn)) {
        return providerIdColumn;
    }
    return undefined;

}

// A function here returns a string array of the keys in Model.
const columnInfo: (keyof Model)[] = ["id", "name", "boardex_id", "source", "duedil_id", "zoom_info_id"];

const source = "boardex";

// Returned values here are fine but I want to get the desired output.
const providerIdColumn = getProviderIdColumn<Model>(columnInfo, source);

Playground link

CodePudding user response:

We can do that by using this mapped type to extract the _id property names:

/**
 * Gets a union of the literal types in `T` that end with `_id`.
 */
type IdKeys<T extends any[]> = keyof {
    [Key in T[number] as Key extends `${string}_id` ? Key : never]: null;
};

and then use that in the function:

/**
 * Takes a list of columns for a specific table and a source and checks for any columns that
 * match `${source}_id`.
 * @template ObjectType The desired output type
 * @param possibleColumns string array of columns of type T
 * @param source The source to convert in question
 * @returns a key of one of the columns of T or undefined if not matched
 */
export function getProviderIdColumn<ObjectType>(
    possibleColumns: (keyof ObjectType)[],
    source: string
): IdKeys<(keyof ObjectType)[]> | undefined {
    // Sadly, we have to have a type assertion, since `source` is type `string`
    const providerIdColumn = `${source}_id` as IdKeys<(keyof ObjectType)[]>;
    if (possibleColumns.includes(providerIdColumn)) {
        return providerIdColumn;
    }
    return undefined;
}

The result is that in your sample call:

const providerIdColumn = getProviderIdColumn<Model>(columnInfo, source);

...providerIdColumn's type is "boardex_id" | "duedil_id" | "zoom_info_id" | undefined.

Playground example

  • Related