Если вы не против использовать кортеж пути, то нам нужно манипулировать кортежами. Полезным псевдонимом типа является 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
constains литералы, а не только string
. И | []
в ограничении для K
помогает компилятору вывести кортеж вместо массива. Вы хотите, чтобы ["user", "address"]
выводился как тип ["user", "address"]
, а не как string[]
, иначе все развалится. Эта раздражающая магия - тема microsoft / 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}}}
Думаю, неплохо выглядит. Хорошо, надеюсь, что это поможет; удачи!
площадка ссылку на код
person
jcalz
schedule
03.03.2020
"user.address"
, поскольку компилятор не может анализировать строки на уровне типа. Вместо этого вам нужно будет использовать что-то вроде кортежей в форме["user", "address"] as const
, чтобы даже начать писать это. Или вы можете сделать параметрmerge
отдельным объектом, например{user: { address: {door: 59}}}
, и вообще не упоминать путь (в этом случае тип возвращаемого значения действительно будет выглядеть примерно какT & M
). Я был бы рад посоветовать любой из этих вариантов, но строковые литералы пути с точками не подходят. - person jcalz   schedule 03.03.2020"user.address"
сработает. Спасибо за объяснение. Я могу это сделать["user","address"]
. Было бы здорово, если бы вы помогли мне внести это новое изменение. - person Abhishek Saha   schedule 03.03.2020