Reputation: 995
We're looking for a type safe way of using Object.assign. However, we can't seem to make it work.
To show our problem I'll use the copyFields method from the Generics documentation
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = source[id];
}
return target;
}
function makesrc(): Source { return {b: 1, c: "a"}}
interface Source {
a?: "a"|"b",
b: number,
c: "a" | "b"
}
I want the engine to prevent me from creating undeclared properties
/*1*/copyFields(makesrc(), {d: "d"}); //gives an error
/*2*/copyFields(makesrc(), {a: "d"}); //gives an error
/*3*/copyFields(makesrc(), {c: "d"}); //should give an error, but doesn't because "a"|"b" is a valid subtype of string.
//I don't want to specify all the source properties
/*4*/copyFields(makesrc(), {b: 2}); //will not give me an error
/*5*/copyFields(makesrc(), {a: "b"}); //should not give an error, but does because string? is not a valid subtype of string
We have attempted to solve this with explicitly providing the types to the copyfields call but we can't find a call that will make all examples work.
For example: to make 5 work you might call copyFields like this:
/*5'*/copyFields<Source,{a?:"a"|"b"}>(makesrc(), {a: "b"});
but subsequent changes to the Source type (such as removing the "b" option) will now no longer result in a type error
Does anyone know of a way to make this work?
Upvotes: 5
Views: 4201
Reputation: 181
You can use Object.assign<TargetType, SourceType>(target, source)
- I think it provides type safety.
Upvotes: 3
Reputation: 736
I've done this solutions time ago:
/**
* assign with known properties from target.
*
* @param target
* @param source
*/
public static safeAssignment(target: any, source: any) {
if (isNullOrUndefined(target) || isNullOrUndefined(source)) {
return;
}
for (const att of Object.keys(target)) {
target[att] = source.hasOwnProperty(att) ? source[att] : target[att];
}
}
I hope that someone can be useful. Regards
Upvotes: 0
Reputation: 1319
Typescript 2.1.4 to the rescue!
interface Data {
a?: "a"|"b",
b: number,
c: "a" | "b"
}
function copyFields<T>(target: T, source: Readonly<Partial<T>>): T {
for (let id in source) {
target[id] = source[id];
}
return target;
}
function makesrc(): Data { return {b: 1, c: "a"}}
/*1*/copyFields(makesrc(), {d: "d"}); //gives an error
/*2*/copyFields(makesrc(), {a: "d"}); //gives an error
/*3*/copyFields(makesrc(), {c: "d"}); //gives an error
//I don't want to specify all the source properties
/*4*/copyFields(makesrc(), {b: 2}); //will not give me an error
/*5*/copyFields(makesrc(), {a: "b"}); //will not give me an error
Upvotes: 1
Reputation: 276135
I have this function:
/**
* Take every field of fields and put them override them in the complete object
* NOTE: this API is a bit reverse of extend because of the way generic constraints work in TypeScript
*/
const updateFields = <T>(fields: T) => <U extends T>(complete: U): U => {
let result = <U>{};
for (let id in complete) {
result[id] = complete[id];
}
for (let id in fields) {
result[id] = fields[id];
}
return result;
}
Usage:
updateFields({a:456})({a:123,b:123}) // okay
updateFields({a:456})({b:123}) // Error
🌹.
I've mentioned this function before in a different context : https://stackoverflow.com/a/32490644/390330
PS: things will get better once JavaScript gets this to stage 3 : https://github.com/Microsoft/TypeScript/issues/2103
Upvotes: -1
Reputation: 4412
The best workaround I can think of is to define a second interface (I called it SourceParts
) that is exactly the same as Source
, except that all members are optional.
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = source[id];
}
return target;
}
function makesrc(): Source { return {b: 1, c: "a"}}
interface Source {
a?: "a"|"b",
b: number,
c: "a" | "b"
}
interface SourceParts {
a?: "a"|"b",
b?: number,
c?: "a" | "b"
}
/*1*/copyFields<Source, SourceParts>(makesrc(), {d: "d"}); //gives an error
/*2*/copyFields<Source, SourceParts>(makesrc(), {a: "d"}); //gives an error
/*3*/copyFields<Source, SourceParts>(makesrc(), {c: "d"}); //gives an error
//I don't want to specify all the source properties
/*4*/copyFields<Source, SourceParts>(makesrc(), {b: 2}); //will not give me an error
/*5*/copyFields<Source, SourceParts>(makesrc(), {a: "b"}); //will not give me an error
Here it is on the Typescript Playground.
Upvotes: 1