Typescript prevents you from prototyping
I tend to prototype things in JavaScript, then re-write them in TypeScript for production.
or
Flow is better than Typescript because it doesn't prevent you from prototyping. Without
@flow
annotation there is no compile errors.
I hear this quite often, and I can’t agree on that.
First of all, javascript is emitted despite compile errors. That is default behaviour unless you have explicitly told the compiler to --noEmitOnError
.
Secondly, Typescript is never a complication for me. Actually, I start prototyping by defining objects’ shapes, messages’ types, constants and adding libraries’ type definitions. And that gives me control over the things I’m doing.
Typescript is not just a transpiler (even though it can be used just for modern-to-old transformation). There are a lot more things included. It’s a type checker and, what’s the most important, great toolbox and IDE.
While prototyping entities often change its meaning, so I have to rename them or move to a more suitable place. These operations are safe as long as you provide type information to the compiler. Names and types are being automatically imported while you type it. Enum’s and object’s properties are auto-completed. And it works the way you expect it. As a Javascript developer, I never was so confident about my code, and especially about refactorings’ safety. I can go to bed without being afraid to forget everything by next morning.
Once you have that experience, there is no way back.
Broken `Export Default`
Javascript Modules Crash Course
There are a couple of ways to define and require modules in Javascript.
First, CommonJS:
const utils = require("src/utils");
const translate = require("src/translate");
utils.log(translate("All set"));
module.export = {}; // exported value
The standard is implemented in node
.
Second, AMD (Asynchronous Module Definition):
define(
["src/utils", "src/translate"],
(utils, translate) => {
utils.log(translate("All set"));
return {}; // exported value
},
)
The most popular implementation is Require.js.
Addy Osmani overviewed both in more detail.
The Good and the Bad
Original AMD syntax looks too verbose and it’s inconvenient to add/remove dependencies if you have a number of them.
That’s why Require.js supports CommonJS syntax since the first release. So everyone was using CommonJS before tc39 proposed new syntax in ES6.
ES6 Imports
In 2015 we got a module system that looked exciting after all. I mean, AMD is totally weird, CommonJS allows dynamic and conditional imports as well as exports and uses globally defined module
so none of them is good enough.
In my opinion, import statements should not look like an ordinary expression. It should not be allowed inside of a block, and it should give you more flexibility as qualified and named imports.
In ES6 we have all that and even more.
import React from "react";
import * as utils from "../utils";
import { foo } from "../foo";
According to the spec there are named and default exports. For example, if you have React+Redux app, you may want to export component as named member and connected component as default member.
export class FooCmp extends ...
// Named export
// import { FooCpm } from "..."
export default @connect() FooCmp
// Default export
// import FooCmp from "..."
The Ugly
However, ES6 imports are incompatible with CommonJS. Yes, the committee has accepted the module system for javascript that is incompatible with node
.
It turns out there is no way to import default member of a node module because there is no such thing in CommonJS. Simply put, default export is a named export with a name default
. And the line import React from "react"
above fails in runtime unless package’s developer has exported everything under name default
.
module.exports = { foo, bar }
module.exports.default = module.exports
Obviously, you can’t know that without looking into packages’ source code. So just in case, we have to use namespace import rather than default import * as React from "react"
for packages.
However (again!), you may be using your own packages where ES6 modules with default exports are used. Which means you import differently depending on packages’ origin:
// that is CommonJS package, use qualified export
import * as React from "react";
// and that is my package
import Audio from "my-components/Audio";
Also, magic is gone when it comes to re-exporting ES6 modules. export * from "./Video"
takes the ./Video
module and exports everything from current module. Everything except default member, so you have to be explicit about your defaults:
export * from "./Video";
export { default } from "./Video";
Now Video
’s default member became default member of the current module. Apparently, you can have only one default export per file. So to re-export several default members you would have to turn them into named ones:
export { default as Audio } from "./Audio";
export { default as Video } from "./Video";
Another example where we are hostages of unnecessary flexibility is our inability to rely on search. For example, if you import a too generally named module, say, utils
, you may want give it move specific name. If you import from a module-with-dashes
you can give it kebab-case
or PascalCase
name.
import leftPad from "left-pad"
import brangindUtils from "../branding/utils"
That is a problem because developers rely on code search a lot.
Let’s take another look at React. That’s what I found in Flow documentation.
Note: We import
React
as a namespace here withimport * as React from 'react'
instead of as a default withimport React from 'react'
. When importing React as an ES module, you may use either style, but importing as a namespace gives you access to React’s utility types.
They do have default export, but there is no way to understand what’s in here unless you have this red.
Most of the developers will never notice the problem because they use Babel which handles CommonJS modules with additional checks:
import * as React from "react";
import utils from "utilsWithDefaultExport";
export default utils.decorate(React);
// compiles to
Object.defineProperty(exports, "__esModule", {
value: true
});
var React = _interopRequireWildcard(require("react"));
var _utilsWithDefaultExport = require("utilsWithDefaultExport");
var _utilsWithDefaultExport2 = _interopRequireDefault(_utilsWithDefaultExport);
// ...
exports.default = _utilsWithDefaultExport2.default.decorate(React);
On one side it is good, so you don’t have to think about it, but this behaviour is not compatible with the specification. On another side, you are in trouble once you switch to another transpiler.
On recap
Default export:
- incompatible with CommonJS
- makes your code entangled
- breaks your search
- syntax sugar with (almost) no benefit
That is a brilliant example of accidental complexity being added with no good motivation.
I wish I would have never used default exports. And apparently, I’m staying away from them.