renevanderark
renevanderark

Reputation: 995

Safe typing of Object.assign

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

Answers (5)

evgeni tsvetanov
evgeni tsvetanov

Reputation: 181

You can use Object.assign<TargetType, SourceType>(target, source) - I think it provides type safety.

Upvotes: 3

hizmarck
hizmarck

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

Jauco
Jauco

Reputation: 1319

Typescript 2.1.4 to the rescue!

Playground link

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

basarat
basarat

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

🌹.

More

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

Frank Tan
Frank Tan

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

Related Questions