Сделать типизацию для ключей объекта после flattenObject

Я не силен в TS настолько, чтобы реализовать такую типизацию, но уже долго мучаю GPT и не могу добиться нужного результата.

type FlattenObject<T> = T extends object
    ? T extends Array<any>
    ? T[ number ]
    : {
        [ K in keyof T ]: T[ K ] extends object
        ? FlattenObject<T[ K ]>
        : T[ K ];
    }
    : T;

type FlattenRecord<T, Prefix extends string | number | symbol = ""> =
    {
        [ K in keyof T ]: T[ K ] extends object
        ? FlattenRecord<T[ K ], `${ Prefix & string }${ "" extends Prefix ? "" : "." }${ Extract<K, string & keyof T> & string }`>
        : T[ K ];
    };

export type Flatten<T> = FlattenObject<T> extends infer O ? FlattenRecord<O> : never;

function flattenObject<T extends object>(object: T, parentKey: string = ""): Flatten<T>
{
    // @ts-ignore
    return Object.keys(object).reduce<Flatten<T>>((acc: any, key: string) =>
    {
        const newKey = parentKey ? `${ parentKey }.${ key }` : key;

        // @ts-ignore
        if (typeof object[ key ] === "object" && object[ key ] !== null)
        {
            // @ts-ignore
            const nestedObject = flattenObject(object[ key ], newKey);

            Object.assign(acc, nestedObject);
        }
        else
        {
            // @ts-ignore
            acc[ newKey ] = object[ key ];
        }

        return acc;

        // @ts-ignore
    }, {});
}

export default flattenObject;

На входе у нас объект который имеет интерфефс:

export interface IMember
{
    _id: string;
    avatar?: string;
    name: Record<"ru" | "en", string>;
    desc: Record<"ru" | "en", string>;
    specialization: Record<"ru" | "en", string>;
}

То есть в теории после Flatten<IMember> я должен получить тип:

{
    _id: string;
    avatar?: string;
    name.ru: string;
    name.en: string;
    desc.ru: string;
    desc.en: string;
    specialization.ru: string;
    specialization.en: string;
}

Ответы (1 шт):

Автор решения: Sdf

Необходимо определить тип Flatten, который будет отражать операцию "плоского" преобразования объекта. Этот тип должен корректно обрабатывать вложенные объекты и массивы, создавая из них тип с "плоскими" ключами.

type Primitive = string | number | boolean | bigint | symbol | undefined | null;

type Flatten<T> = T extends Primitive | Function | Date | Array<any> ? T : {
    [K in keyof T as (T[K] extends object ? `${string & K}.${FlattenKeys<T[K]>}` : string & K)]: T[K] extends object ? Flatten<T[K]> : T[K]
}[keyof T];

type FlattenKeys<T> = T extends object ? {
    [K in keyof T]: T[K] extends object ? `${string & K}.${FlattenKeys<T[K]>}` : string & K
}[keyof T] : '';

export type Flatten<T> = FlattenRecord<FlattenObject<T>>;

function flattenObject<T>(obj: T, prefix: string = ''): Flatten<T> {
    let result: any = {};

    for (const [key, value] of Object.entries(obj)) {
        const flatKey = prefix ? `${prefix}.${key}` : key;
        
        if (typeof value === 'object' && value !== null && !(value instanceof Date) && !(value instanceof Array) && !(value instanceof Function)) {
            Object.assign(result, flattenObject(value, flatKey));
        } else {
            result[flatKey] = value;
        }
    }

    return result;
}

Для примера:

const nestedObj = {
    a: 1,
    b: {
        c: 2,
        d: {
            e: 3
        }
    }
};

const flatObj = flattenObject(nestedObj);
console.log(flatObj);

→ Ссылка