Построить тип пересечения без копирования объекта

#typescript

#typescript

Вопрос:

У меня есть вызов службы, который выдает мне объект, и я хочу сохранить больше данных в этом объекте. Я максимально абстрагировал его:

ссылка на игровую площадку

 //in the real sample, I1 and the serviceCall are generated and I do not want to change them
interface I1{ name: string; }
function serviceCall():I1 {return {name:"Hello World"};}

const o1:I1 = serviceCall();

interface I2 { age: number; }

// I want to store the data of I2 in the same object, so my target type is (I1amp;I2)
type target = (I1amp;I2);

//solution 1: this copies all of o1 into a new object. I want no extra copy!
const o2copy:target = {...o1, age: 144};

//solution 2: casting
const o2cast:target = o1 as target;
//works but does not check anything! 
//age is undefined until I set it but it does not give me any error if I leave the next line!
o2cast.age = 144;

//solution 3: simply setting the property: the javascript is fine, but typescript yields errors
o1.age = 144; //ERROR: property age does not exist on I1
const o2setprop:target = o1;
 

решение 3 было бы моим любимым, как я могу сделать эту компиляцию?

Комментарии:

1. Вы не можете выполнить эту компиляцию, вы либо выполняете приведение и гарантируете, что код безопасен вручную, если у вас действительно горячий цикл или что-то в этом роде, либо просто копируете, потому что это javascript, и последствия для производительности одной мелкой копии, вероятно, не имеют значения.

2. в действительности у меня есть массив объектов, и каждый объект также содержит больше данных. Я максимально уменьшил проблему здесь.

Ответ №1:

Вы можете использовать Object.assign() для копирования свойств из {age: 144} в ваш целевой o1 объект. Эта функция возвращает o1 , и возвращаемый тип сигнатуры типа стандартной библиотеки для нее является типом пересечения, по желанию:

 const o2: target = Object.assign(o1, { age: 144 });
console.log(o2.name.toUpperCase()); // HELLO WORLD
console.log(o2.age.toFixed(1)); // 144.0
 

Обратите внимание, что пересечение — это всего лишь приближение к фактическому типу, который Object.assign() возвращается. Если вы передаете параметры с противоречивыми свойствами, пересечение будет неточным. Однако в вашем примере кода это не проблема.

Однако компилятор все равно будет видеть o1 как тип I1 , поэтому вам придется начать использовать o2 исключительно и забыть о o1 :

 o1.age; // compiler error!
// ~~~ <-- I1 has no property named "age"
 

Если вы действительно хотите сохранить o1 его без необходимости использовать для него новое имя переменной, вы можете рассмотреть возможность использования функций утверждения, представленных в TypeScript 3.7. Встроенной версии Object.assign() , которая действует как функция утверждения, нет, но вы можете написать ее:

 function assign<T, U>(target: T, source: U): asserts target is T amp; U {
    Object.assign(target, source);
}
 

Возвращаемый тип assign is asserts target is T amp; U . Таким образом, после вашего вызова assign(o1, {age: 144} компилятор будет рассматривать o1 как I1 amp; {age: number} :

 assign(o1, { age: 144 });
console.log(o1.name.toUpperCase()); // HELLO WORLD
console.log(o1.age.toFixed(1)); // 144.0
 

Возможно, вы хотите поступить именно так, но имейте в виду, что реализации функций утверждения не проверяются компилятором как безопасные для типов. Автор такой функции несет ответственность за то, чтобы после вызова функции действительно происходило сужение типа, указанное в ее возвращаемом типе. Ничто не предупредит вас, если, например, вы ничего не сделали внутри функции:

 function badAssign<T, U>(target: T, source: U): asserts target is T amp; U { }
 

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

 badAssign(o1, { oops: "uh oh" });
o1.oops.toUpperCase(); // no compiler error, but runtime error!
 

Так что будьте осторожны с этим подходом.


Игровая площадка ссылка на код

Комментарии:

1. Я знал о функциях утверждения, но мне никогда не приходило в голову, что они могут изменять «утвержденный» объект! При правильном именовании каждый член команды должен понимать, что он делает. спасибо за ваш подробный ответ!

Ответ №2:

Я думаю, вы можете это сделать:

 const o2setprop:target = setProp(o1, "age", 144);


function setProp<T, P, K extends keyof any>(obj: T, k: K, val: P): T amp; { [_ in K]: P } {
    const o = obj as any
    o[k] = val
    return o
}
 

Комментарии:

1. Мне больше нравится подход @jcalz, поскольку проще добавить более одного свойства за один раз. Я думаю, что мой вопрос в этом отношении был упрощен. Спасибо за ваш ответ. Пожалуйста, скажите мне, вам нужно было посмотреть описание этого типа, это действительно куча описаний 🙂

2. @fabsenet Это довольно простое определение типа, если вы знаете, как использовать сопоставленные типы. Сопоставленные, условные и рекурсивные типы, безусловно, являются лучшей функцией typescript, которую никто не знает, как использовать, имхо, каждый должен научиться!