5

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?

renevanderark
  • 975
  • 7
  • 15
  • 1
    Take a look at `&` type operator. –  Aug 18 '16 at 08:42
  • BTW we (Rene is my coworker) found that flow does support this use case https://tryflow.org/?code=ZGVjbGFyZSBpbnRlcmZhY2UgU291cmNlIHsKICAgIGE/OiAiYSIgfCAiYiIsCiAgICBiOiBudW1iZXIsCiAgICBjOiAiYSIgfCAiYiIKfQpmdW5jdGlvbiBtYWtlc3JjKCkgOiBTb3VyY2UgeyByZXR1cm4ge2I6IDEsIGM6ICJhIn19CgovKjEqL09iamVjdC5hc3NpZ24obWFrZXNyYygpLCB7ZDogImQifSk7Ci8qMiovT2JqZWN0LmFzc2lnbihtYWtlc3JjKCksIHthOiAiZCJ9KTsKLyozKi9PYmplY3QuYXNzaWduKG1ha2VzcmMoKSwge2M6ICJkIn0pOwoKLyo0Ki9PYmplY3QuYXNzaWduKG1ha2VzcmMoKSwge2I6IDJ9KTsKLyo1Ki9PYmplY3QuYXNzaWduKG1ha2VzcmMoKSwge2E6ICJiIn0pOw== – Jauco Aug 18 '16 at 08:43
  • @torazaburo that will not make the required examples fail – Jauco Aug 18 '16 at 08:44
  • 1
    Possibly relevant: http://stackoverflow.com/questions/29207118/typescript-adding-only-an-optional-property-removes-type-checking –  Aug 18 '16 at 18:30

5 Answers5

3

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

evgeni tsvetanov
  • 173
  • 1
  • 13
  • 1
    If you combine it with Partial, you won't get the IDE complaining about missing required fields. Eg: `Object.assign>(myObj, {})` – CrazyPenguin Jun 07 '22 at 14:58
1

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.

Frank Tan
  • 4,234
  • 2
  • 19
  • 29
1

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
Jauco
  • 1,320
  • 13
  • 23
0

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

hizmarck
  • 686
  • 9
  • 19
-1

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

Community
  • 1
  • 1
basarat
  • 261,912
  • 58
  • 460
  • 511
  • nice solution, however, our wish is (as mentioned in the comment): "I don't want to specify all the source properties" – renevanderark Aug 19 '16 at 09:47
  • 1
    @renevanderark don't have to `updateFields({a:456})({a:123,b:123}) ` only `a` is specified (the source has both `a` and `b`) – basarat Aug 19 '16 at 22:05
  • 1
    I'm sorry. That function is really the same as ours, but with currying. I tried it in the playground (link is too long to paste here) and it fails on the same tests (3 & 5) – Jauco Aug 22 '16 at 15:41