Typescript: Any roadmap on supporting Object.assign?

Created on 9 Jun 2015  ·  47Comments  ·  Source: microsoft/TypeScript

in 1.6 or 2.0?

Question

Most helpful comment

I'm sorry I don't understand. ES6 supports Object.assign so Babel transpiles it to a polyfill for ES5 targets. I'm confused why TypeScript doesn't do the same, just as it does for other ES6 features.

All 47 comments

not sure what you are looking for. if you mean the typings, it is already defined in lib.es6.d.ts as

    /**
      * Copy the values of all of the enumerable own properties from one or more source objects to a 
      * target object. Returns the target object.
      * @param target The target object to copy to.
      * @param sources One or more source objects to copy properties from.
      */
    assign(target: any, ...sources: any[]): any;

i you mean the implementation, TypeScript does not include pollyfills, you can always include yours, for instance Mozilla has a pollyfill for it.

If you are asking about the the mixin pattern typingings, it is something that we always had on the roadmap for 2.0.

Thanks. I mean writing Object.assign(...) in TypeScript which transpile to es3/es5/es6. So it is on the roadmap for 2.0.

:+1:

@unional I may have misunderstood something. But I don't think your conclusion is correct.

ES6 anyway supports Object.assign, so if your target is ES6, you should be able to write it in TypeScript anyhow.

But if you transpile to es5 and below, you'd still need a polyfill, because es5 doesn't have Object.assign.

As a quick workaround consider this polyfill and typing:

interface ObjectConstructor {
    assign(target: any, ...sources: any[]): any;
}

if (typeof Object.assign != 'function') {
  (function () {
    Object.assign = function (target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert undefined or null to object');
      }

      var output = Object(target);
      for (var index = 1; index < arguments.length; index++) {
        var source = arguments[index];
        if (source !== undefined && source !== null) {
          for (var nextKey in source) {
            if (source.hasOwnProperty(nextKey)) {
              output[nextKey] = source[nextKey];
            }
          }
        }
      }
      return output;
    };
  })();
}

Thanks for digging up this issue. It was quite a while ago. :) Yes, I now understand that it simply requires a polyfill for es5.

Thanks again!

Will we compile this polyfill when target is set to es5? I mean, since the compilation target is set to es5, then this should be able to work under es5 environment.

Will we compile this polyfill when target is set to es5

Yes, as a ponyfill :rose: :horse:

I'm sorry I don't understand. ES6 supports Object.assign so Babel transpiles it to a polyfill for ES5 targets. I'm confused why TypeScript doesn't do the same, just as it does for other ES6 features.

@prashaantt TypeScript doesn't provide any polyfills and it's by design. If you want to use Object.assign or any other primitive/method added by newer standard you should either:

  • use polyfill library (like core-js) with corresponding typings
  • target the standard when the feature was added (e.g. "target": "es6" in your tsconfig.json)

@devoto13 Thanks, core-js works well.

I just had a silly question. After npm installing and typings installing core-js I begin to get IntelliSense for the polyfilled methods. But I will still need to actually import those methods in every module where I'm using them or the compiled code wouldn't include the polyfills, right?

@prashaantt Once core-js/shim is required by anything, Object.assign will be available globally from that point onwards. I recommend putting import 'core-js/shim'; at the top of your main module/entry point.

Thanks @jesseschalken. As a follow up, isn't importing the whole shim going to bloat up my bundle? Or will tsc or ts-loader be smart enough to only pick up the things that actually get used in my code?

@prashaantt It depends on the browsers you're targeting. You already know Object.assign is not supported by a browser you're targeting, who knows what else isn't? You need the whole shim in your bundle if you want the widest browser support.

If you only want the polyfill for Object.assign, you can import 'core-js/modules/es6.object.assign';, and add more things as you discover you need them (see shim.js in core-js for a list, also the docs). Webpack will follow the require graph and only include the modules necessary.

If you're already using Babel, I recommend using import 'babel-polyfill'; instead of using core-js directly. It includes core-js/shim but also regenerator-runtime for generators/async-await.

Thanks for the tips, though we should have full generators support any moment now — literally the last hurdle to 2.0!

I'm sorry I don't understand. ES6 supports Object.assign so Babel transpiles it to a polyfill for ES5 targets. I'm confused why TypeScript doesn't do the same, just as it does for other ES6 features.

@prashaantt What do you mean when you say that Babel _transpiles_ Object.assign? It is just a function. You can add the polyfill, ponyfill, or write it yourself, and you can use it any environment - ES 3, 5.1, 6, etc.

@aluanhaddad My understanding of what babel does is if you specify es5 as your target and use Object.assign, it automatically includes a polyfill for Object.assign, and doesn't if you don't use it. It would be nice if typescript did the same thing, as its claimed that it's a "superset of es2015", which isn't really true if it doesn't provide functionality to transpile to older targets. (I could be wrong though)

@devoto13 if your target is es5, then you should at the very least throw a warning that Object.assign isn't supported in es5. It makes zero sense to have it be completely valid and not telling the programmer that you need some random polyfill.

@kyleholzinger what you've described (targeting ES5 means Object.assign isn't available) is already the behavior.

@kyleholzinger It indeed does throw error. If you create folder with two following files:

// test.ts
let t = Object.assign({}, {});
// tsconfig.json
{ "target": "es5" }

And then run tsc on it. You'll get following error:

$ tsc
test.ts(1,16): error TS2339: Property 'assign' does not exist on type 'ObjectConstructor'.

In your project you probably include typings for some polyfill, but don't include polyfill implementation, that's why it fails.

I understand that it's the current behavior haha. My point is that if you specify the target as es5, it'd be nice if typescript gave you a meaningful error beyond "it's not on the constructor".

@kyleholzinger FWIW, TypeScript 2.1 now supports the ES6 (ES7?) object rest/spread so I personally find lesser reason to bother about Object.assign anymore. That with native generators means I don't need any polyfills in most of my projects.

That's true. It'd just be nice to not leave out language features. It'd be awesome if Object.assign wasn't browser dependent and typescript warned you if you weren't using a polyfill.

If your tsconfig is set right, TypeScript warns you about assign not being available for < ES6 by not giving you the option to autocomplete that function name on Object in the first place. If you persist in painstakingly writing it out by hand, you'll see the red squiggles. If you ignore that, tsc will give you the above error. But if you willfully ignore that too, you rightfully deserve your doom. ;)

right, my only point is in the es2015 spec, so it should be in typescript ;)

How do I use Object.assign on node while targeting es5 ? That is, code is to run on server and not in the browser. Do I use polyfills too and how?

@johnbendi
It depends on the version of node, which is to say that it depends on the runtime as is the case with all polyfillable functionality.

Here is how you can do test if your runtime supports Object.assign

$ Node 
> Object.assign({ to: 'world' }, { message: 'Hello there!' })
{ to: 'world', message: 'Hello there!' }

If the above works all you need to do is include "es2017.object" in the "compilerOptions"."lib" property of your tsconfig.json.

If it the above fails, add a polyfill like this one, which is written in TypeScript.

// polyfill-object-assign.ts

if (typeof Object.assign !== 'function') {
  Object.assign = function (target, ...args) {

    if (!target) {
      throw TypeError('Cannot convert undefined or null to object');
    }
    for (const source of args) {
      if (source) {
        Object.keys(source).forEach(key => target[key] = source[key]);
      }
    }
    return target;
  };
}

And import it with

import './polyfill-object-assign';

And also make the same changes to your tsconfig.json as for the runtime supported case.

I hope that helps

@aluanhaddad thanks a lot for the insights. My node does support Object.assign based on the experiment you asked me to run. But even after adding the "compilerOptions": { "lib": ["es2017.object"] } I still get the squiggles. Should I just ignore it or is there something I can do to make it go away.

@aluanhaddad never mind. It works well now.

@aluanhaddad I previously had "compilerOptions": { "lib": ["es2017.object", "es6"] } when I was getting the squiggles. Removing es6 seemed to resolve but rerunning my gulp script again suddenly produces a new set of errors.
My tsconfig.json :

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "lib": ["es2017.object"],
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "sourceMap": true,
        "inlineSources": true,
        //"noImplicitAny": true,
        "declaration": true,
        "noFallthroughCasesInSwitch": true,
        // "noImplicitReturns": true,
        "removeComments": true,
        "stripInternal": true,
        "outDir": "dist"
    },
    "files": ["src/main.ts"],
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "node_modules"
    ]
}

Sample of my new errors:

error TS2318: Cannot find global type 'Array'.
error TS2318: Cannot find global type 'Boolean'.
error TS2318: Cannot find global type 'Function'.
error TS2318: Cannot find global type 'IArguments'.
error TS2318: Cannot find global type 'Number'.
error TS2318: Cannot find global type 'RegExp'.
error TS2318: Cannot find global type 'String'.
error TS2339: Property 'bind' does not exist on type '(message?: any, ...optionalParams: {}) => void'.
error TS2339: Property 'bind' does not exist on type '(message?: any, ...optionalParams: {}) => void'.
error TS2322: Type '{}' is not assignable to type
error TS2304: Cannot find name 'Promise'.

So is there a way to use your recommendation and still get typescript to compile correctly?

@johnbendi yes, certainly.

Use I was simply suggesting adding the specific entry "es2017.object" because of the specificity of your request.
I believe "lib": ["es6"] is no longer correct and that it should be "lib": ["es2015"].
Try "lib": ["es2015", es2017.object"] or just "lib": ["es2017"].

I think what's missing is a clean way to say "I have an es6 polyfill and it's not your bussiness typescript, just assume I do" :).

Because, setting target: "es6" does that, but presumably can also generate code using non-polyfillable es6 features.

Requiring core-js explicitly essentially forces you to have the shim, you can't have shimmy and non-shimmy builds because TS will complain.

Adding node_modules/typescript/lib/lib.es6.d.ts to files in tsconfig.json does that, but.. does not look so clean... (or am I missing an obvious way of achieving that?)

@himdel just use

{
  "compilerOptions": {
    "lib": [
      "es2015"
    ]
  }
}

It works perfectly - I've been doing it for months.

Or any subset you wish:

{
  "compilerOptions": {
    "lib": [
      "es2015.core",
      "es2016.array.include"
    ]
  }
}

I still cannot figure out how to do this. When i have target: "es5" tsc will always transform Object.assign into some call to a polyfill. Adding libs does not change anything at all for me.

In my case I want arrow functions to be converted to normal functions, but leave static calls like Object.assign and Array.includes untouched.

tsc should have a flag to tell it to only transpile syntax features and not polyfill features like static methods Object, Array etc.

When i have target: "es5" tsc will always transform Object.assign into some call to a polyfill.

@danez TypeScript won't modify a call to Object.assign. It sounds like you might be running your code through Babel?

@RyanCavanaugh It does. Babel is not involved in the process.
What it does is basically transforming this:

var a = Object.assign({}, {});

into this

var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var a = _assign({}, {});

@danez Can you post an actual repro? The compiler did not emit that code

I don't have a repo, but this is the tsconfig:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "module": "ES2015",
    "target": "es5",
    "outDir": "js/lib/es"
  },
  "include": [
    "js/**/*.ts",
  ],
  "exclude": [
    "**/__tests__/**/*.ts"
  ]
}

and then i simply call tsc

@danez I'm afraid you'll have to post an actual repro.

C:\Throwaway\oat>type a.ts
var a = Object.assign({}, {});

C:\Throwaway\oat>type tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "module": "ES2015",
    "target": "es5",
    "outDir": "js/lib/es"
  },
  "include": [
    "*.ts",
  ]
}
C:\Throwaway\oat>tsc
a.ts(1,16): error TS2339: Property 'assign' does not exist on type 'ObjectConstructor'.

C:\Throwaway\oat>type js\lib\es\a.js
var a = Object.assign({}, {});

@unional in that it is working properly. The compiler has a helper for providing syntactical support, but it does not _ever_ rewrite functionality on globals. @danez that helper is not ever accessible by TypeScript code.

const foo = { foo: 'bar' };
const bar = { ...foo };

will emit:

var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var foo = { foo: 'bar' };
var bar = __assign({}, foo);

I don't think you can provide an example where Object.assign() gets rewritten to __assign, it only gets used in object spread syntactical support.

@kitsonk Yes I was using object spread, you are right. So would be nice if object-spread would just be simply transformed to Object.assign

@danez it is when you target something that supports Object.assign() (e.g. target is es2015+).

For example, this:

const foo = { foo: 'bar' };
const bar = { ...foo };

Will output:

const foo = { foo: 'bar' };
const bar = Object.assign({}, foo);

@kitsonk Yes I know, but I'm targeting es5 _syntax_ with all ES2017+ globals being polyfilled by core-js. So what I'm referring to what would be nice is a mode that outputs es5 syntax but assumes all builtins are available. Similar to what babel is doing with the useBuiltins option: https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx#usebuiltins

In general we don't go out of our way to give special support to "mixed" runtime targets, but there are other options available.

You can inject __assign into the global scope (along with any other helpers, e.g. __extends) and run with --noEmitHelpers.

Yes I know, but I'm targeting es5 _syntax_

I feel like this is the main problem with not shimming features like Object.assign. Gotta take a stance one way or another-- you can't replace its usage with __assign when using a spread but not when you call it directly, it's confusing as hell

you can't replace its usage with __assign when using a spread but not when you call it directly, it's confusing as hell

I can understand you feel it is confusing, but it isn't if you keep the mantra, TypeScript provides _syntatical rewrites_ not _functional polyfills_. TypeScript is full consistent in how it behaves, and it is opinionated about what it does, and 99% of the time it has no impact on the end user.

As @RyanCavanaugh says, it is possible to leverage a set of polyfills, plus tslib, with --noEmitHelpers plus a global script that says:

__assign = Object.assign;

But that is really "bonus round" TypeScript and arguable that it would provide any real world measurable performance improvement.

Was this page helpful?
0 / 5 - 0 ratings