Функция Typescript объединяет объект по указанному c пути - PullRequest
1 голос
/ 03 марта 2020

У меня есть функция слияния, которая объединяет объекты по заданному c пути.

const merge = (src, path, newObj) => {
   // this code works fine
}

Чтобы использовать эту функцию, я вызываю ее так:

interface IUser {
   user: {
      address: {
        street: string
      }
   }
}

interface IAddrNum {
   door: number
}

const User: IUser = {
   user: {
     address: {
        street: "New Street"
     }
   }
}


const mergeObj: IAddrNum = {
   door: 59
}

const newObj = merge(User, "user.address", mergeObj);

С этим, Я получаю правильный результат.

{
   user: {
     address: {
       street: "New Street"
       door: 59
     }
   }
}

Вопрос: Я хочу создать подпись для этой функции в машинописном тексте.

interface Immutable {
   merge<T, K, M>(
     src: T,
     path: K,
     merge: M
   ): T & M // <== this is where the problem is
}

Это не работает должным образом. Это не может быть T & K, потому что слияние должно происходить по указанному пути c. Возможно ли иметь подпись этой функции? Если да, можете ли вы дать мне какое-то направление. Спасибо.

1 Ответ

1 голос
/ 04 марта 2020

Если вы в порядке с использованием кортежа пути, тогда нам нужно манипулировать кортежами. Полезный псевдоним типа - Tail<T>, который принимает тип кортежа, например [string, number, boolean], и возвращает другой кортеж с удаленным первым элементом, например [number, boolean]:

type Tail<T extends any[]> = 
  ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;

. Затем мы можем написать DeepRecord<K, V>, где K - это путь ключей свойств, а V - это некоторое значение. Мы создаем рекурсивный сопоставленный условный тип, который создает тип объекта, в котором V расположен вниз по пути, описанному K:

type DeepRecord<K extends PropertyKey[], V> =
    K extends [] ? V : { [P in K[0]]: DeepRecord<Tail<K>, V> };

Просто чтобы убедиться, что это работает, вот пример "

type Example = DeepRecord<["foo", "bar", "baz"], string>;
/* type Example = { foo: { bar: { baz: string; }; };} */

Теперь мы, по сути, закончили, но также неплохо сделать что-то, что рекурсивно объединяет пересечения, чтобы вместо {foo: {bar: {baz: string}}} & {foo: {bar: {qux: number}}} вы получили один {foo: {bar: {baz: string; qux: number;}}}:

type MergeIntersection<T> = 
  T extends object ? { [K in keyof T]: MergeIntersection<T[K]> } : T;

Наконец , мы можем дать merge() сигнатуру типа (и реализацию, хотя это выходит за рамки вопроса и не гарантированно будет правильным):

const merge = <T extends object, K extends N[] | [], 
  V extends object, N extends PropertyKey>(
    src: T, path: K, newObj: V
) => {
    const ret = { ...src } as MergeIntersection<T & DeepRecord<K, V>>;
    let obj: any = ret;
    for (let k of path) {
        if (!(k in obj)) {
            obj[k] = {};
        }
        obj = obj[k];
    }
    Object.assign(obj, newObj);
    return ret;
}

Обратите внимание, что N на самом деле не делает в определении, но это позволяет компилятору сделать вывод, что параметр path содержит литералов , а не только string. А | [] в ограничении для K помогает компилятору сделать вывод кортеж вместо массива. Вы хотите, чтобы ["user", "address"] выводился как тип ["user", "address"] вместо string[], или все это разваливается. Эти раздражающие волхвы c - это топи c из м icrosoft / TypeScript # 30680 и на данный момент это лучшее, что я могу сделать.

Вы можете проверить это на своем примере кода:

const newObj = merge(User, ["user", "address"], mergeObj);
/* const newObj: {
    user: {
        address: {
            street: string;
            door: number;
        };
    };
}*/

console.log(JSON.stringify(newObj));
// {"user":{"address":{"street":"New Street","door":59}}}

Выглядит хорошо, я думаю. Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

...