frankwallis / plugin-typescript

TypeScript loader for SystemJS
MIT License
248 stars 47 forks source link

Using "target": "ES5" still produces ES2015 types in output #166

Closed seangwright closed 8 years ago

seangwright commented 8 years ago

Here is my tsconfig.js

{
    "compilerOptions": {
        "target": "ES5",
        "allowJs": true,
        "module": "system",
        "noImplicitAny": false,
        "sourceMap": true,
        "removeComments": false
    },
    "exclude": [
        "node_modules",
        "jspm_packages",
        "typings/main",
        "typings/main.d.ts"
    ]
}

Here is part of my config.js

System.config({
  baseURL: "/",
  defaultJSExtensions: true,
  transpiler: "ts",
  typescriptOptions: {
    "tsconfig": true
  },

In my final output build.js I end up with code that looks like the following

this.memberService.getCurrentMemberID().then(function (id) {
    _this.appValues.currentAccountID = id;
    return _this.$q.all([_this.loadMember(), _this.loadContacts(), _this.memberService.getRelatedEntities()]);
}).then(function (result) {
    _this.contactsMap = result[1].reduce(function (map, contact) {
        return map.set(contact.id, contact);
    }, new Map());
    _this.relatedEntities = result[2];
}).finally(function () {
    return _this.componentState.isLoading = false;
});

My original Typescript defines an instance of the Map type and that type is not available in ES5 but it ends up in the build.js file. The command I'm running to generate my build.js is jspm bundle app --inject.

Why isn't Typescript transpiling my Map type to something compatible with ES5? If I include babel-polyfills.js in the scripts loaded before build.js then everything runs fine in IE11 but if I don't include it then the app blows up as soon as it hits a map.set() call.

aluanhaddad commented 8 years ago

This is the correct and expected behavior. What follows is an oversimplified explanation:

As languages evolve, they introduce new syntax and semantics.

In tandem they also introduce new functionality in their standard libraries.

For example, in addition to new syntax, such as => and const {x ,y} = z, the ES2015 language specification introduced a new standard library function, Map.

Many new features, be they syntactic additions such as the literal => token, semantics such as the lexical scoping of the this keyword in => functions, or standard functions such as Map, can be expressed in terms of constructs available in older versions of the language such as ES5.1.

In order to perform this process, which I will colloquially refer to as downleveling, different strategies are necessary depending on the kind of feature introduced.

For example, the syntactic and semantic behavior of => can be downleveled via a process called transpilation (compilation). This is the role of a transpiler. A transpiler transforms syntax and associated semantics, expressing them by way of constructs available in the target language.

As Map is not a syntactic construct, it is largely outside the scope of transpilation. Instead, downleveling is achieved via polyfilling. A polyfill is simply a user-provided (e.g. not built in) library that adds to or augments an API such that it behaves like a newer version of the language. There is no syntactic process involved. To downlevel Map we might write a polyfill. For example it might be written (naively) as

window.Map = (function () {
  function Map() {
    this.entries = Array.prototype.slice.call(arguments, 0);
  }
  Map.prototype.forEach = function (action) {
   var _this = this; 
   this.entries.forEach(function (entry) {
      var key = entry[0];
      var  value = entry[1];
      action(key, value, _this);
    });
  };
  // ... rest of implementation
  return Map;
})();

We can add downlevel support for Map by including this code, but this is not transpilation.

You might argue that, whether via a library or via a syntactic transformation, TypeScript as a language supporting ES2015 should provide built in support for Map, but as part of its standard library and include it automatically in all user code using Map but they chose to work with existing polyfills. Regardless this is not a syntactic process. There is nothing to transpile. There is no issue.

seangwright commented 8 years ago

@aluanhaddad Thanks, that's helpful information. I thought transpiling covered all aspects of the language.

That said, could you point me towards the recommended way of currently importing babel-polyfill of core-js into an app? I've tried several variations of imports and jspm i but nothing seems to be pulling the polyfills into my bundle.

I, obviously, am using jspm/systemjs and typescript.

Examples: In my app entrypoint main.ts I have tried the following lines

import "babel/polyfill";
import "babel-polyfill";
import "babel-polyfill/dist/polyfill";

and I have the following installed via jspm

map: {
    "babel": "npm:babel-core@6.17.0",
    "babel-polyfill": "npm:babel-polyfill@6.16.0",
    "babel-runtime": "npm:babel-runtime@5.8.38",
    "core-js": "npm:core-js@1.2.7",

The polyfills never end up in my bundle but if I include them as a separate script tag in my page then everything loads correctly.

aluanhaddad commented 8 years ago

I have a very similar setup on several of my projects (jspm 0.17 + plugin-typescript + core-js. I am not sure in your specific case but it the problem is likely that you have the babe-polyfills installed as dev-dependencies, or possibly that you have certain settings like "babelOptions.include": ["runtime"] configured a certain way. Those polyfills are specific to specific versions of babel and jspm, also, their version numbers seem to be incompatable.

Regardless, for TypeScript, the following should work just fine

~/my-app> jspm install core-js

main.ts

import 'core-js';

// bootstrap application.
seangwright commented 8 years ago

@aluanhaddad Thanks again. Knowing that you were able to get the import working like that led me to investigate other issues. The problem I was experiencing was due to the fact that after I added the polyfill imports I only re-bundled and didn't re-transpile the typescript, so there was no source .js entrypoint file with the import it in.

This all gelling for me now.

One last question, are you using rollup.js/tree shaking in your bundling and if so does that allow you to import only the core-js polyfills that you need? Currently the only es2015 type we are using is Map so it would be great to just import that polyfill. If so is there documentation for configuring this?

--- UPDATE

I replaced the full core-js import with one importing only the Map type

Replace

import "core-js";

with

import "core-js/modules/es6.map";

and both the build.js size and number of files from npm:core-js@2.4.1/modules/ pulled into the bundle has gone down considerably.

frankwallis commented 8 years ago

Just wanted to add - this problem kind of stems from the fact that plugin-typescript defaults to using lib.es6.d.ts (for historical reasons). You can limit the features of es6 which are supported by the type-checker using the new lib option (see here) so if you want to just use Map then it might be lib: [ 'dom', 'es5', 'es2015.collection' ] (I haven't tested this...)

seangwright commented 8 years ago

@frankwallis I was looking into the lib option but I don't really understand what it is doing.

By limiting which features are supported by the type checker what would I gain in my project?

The way I'm reading this, if I used the above lib options then any es2015 type (other than those in es2015.collection would cause an error when compiling my Typescript through the plugin. Is that correct?

frankwallis commented 8 years ago

yes that is correct, it is a way of aligning typescript with what you are planning to provide via polyfills