TypeScript
TypeScript (TS) is a language that promises "JavaScript that scales". After running a number of investigationary projects, TS has been found lacking for a number of reasons that can be summarised in the following categories.
- Configuration
- Permissiveness
- Strict mode
- Lock-in
- Progressive inclusion
At the time of writing, the TS compiler comes with 54 compiler flags and the standard linter, TS Lint, has over 153 rules that can each be configured in a number of ways.
These can often come in conflict and no community standard on what rules or compiler flags to use exist, which has been reflected in twitter polling TS enthusiasts.
Overhead from thinking about configuration is wasted effort, and will require maintenance as new rules and flags are added. This goes against the current trend of attempting to remove build process work from the engineer, with tools such as prettier attempting to have zero configuration, and popular bundlers aiming for similar.
Without strict mode
, TS will allow a large number of type unsafe actions to occur, giving the user the feeling of writing solid code and perhaps forgoing unit tests under the assumption the type system will save them.
Examples of what is allowed by the compiler without strict mode include
- Implicit
any
- Implicit use of
this
- No checking of
null
orundefined
- No checking that properties are initialised
- No checking on function types.
In many ways, this makes TS quite scarily underpowered while giving the impression that type checking has occured.
A strict mode exists to solve all these problems, however it comes with its own problems. It often results in unhelpful error reporting and as it combines type checking with TS Lint passesing, can often waste the developers time in a loop that can only be solved by writing code in an unreasonable way or configuration.
An example of this is given here. You cannot nest interfaces in interfaces, and suggests you use a type. However, if you declare a type, you are told to use an interface instead.
interface IFunction {
(s: string): boolean;
}
interface IObject {
message: string
isMessage: IFunction
}
// Cannot nest interfaces, suggest to use type
type Function {
(s: string): boolean;
}
interface IObject {
message: string
isMessage: Function
}
// Suggests to use interface instead of type for function signature
interface IFunction {
(s: string): boolean;
}
interface IObject {
message: string
isMessage: (s: string): boolean;
}
// A solution that is makes the compiler/default linter happy
An example using Styled-Components
is given. When using this library and writing CSS within a template literal, a mistake will force the compiler to fail.
const RedHeader = styled.h1`
color: red // Error here of no semi colon
padding: 20px;
`;
(7,14): Argument of type 'string' is not assignable to parameter of type 'MutationOptions<any, OperationVariables> | undefined'.
The above does not help you isolate what has gone wrong or the library it has gone wrong in.
The tooling can occasionally fail to watch files, or have conflicts between the TS compiler running the terminal and the linting happening within the editor. Since strict mode requires everything to be in order to compile at all, this can lead to frustations.
In addition, the tooling never suggests how to type somethng, as ReasonML and Flow often do. This leads to the developer having to hunt down typings for things like React events by herself.
const logger = (e: React.FormEvent<HTMLSelectElement>) => console.log("✋", e.currentTarget.value);
// React.FormEvent<HTMLSelectElement> found
These typings are required in strict mode, but not overly discoverable.
Unlike Flow or ReasonML, there is an element of lock in with TS. Should you wish to migrate away from TS, it can be difficult as the compiled output for more complex cases will not be readable or at least pleasent-to-maintain JavaScript.
// Our TS code
class Animal {
constructor(public name: string) { }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
// Generated JavaScript
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Animal = /** @class */ (function () {
function Animal(name) {
this.name = name;
}
Animal.prototype.move = function (distanceInMeters) {
if (distanceInMeters === void 0) { distanceInMeters = 0; }
console.log(this.name + " moved " + distanceInMeters + "m.");
};
return Animal;
}());
var Snake = /** @class */ (function (_super) {
__extends(Snake, _super);
function Snake(name) {
return _super.call(this, name) || this;
}
Snake.prototype.move = function (distanceInMeters) {
if (distanceInMeters === void 0) { distanceInMeters = 5; }
console.log("Slithering...");
_super.prototype.move.call(this, distanceInMeters);
};
return Snake;
}(Animal));
To include TS on an existing project you will need to convert exists JavaScript files manually if you wish to have a fully typed codebase. This stands in contrast to flow, which can infer much of the work by simply placing // @flow
at the top of a file.
In addition, compilation becomes a two step process and requires adding the TS compiler loader into babel, further pushing down on already slow build processes common in modern frontend development.
Re: Lock-in
I don't think that's a valid point.
If you set the target to ES6, you get normal JS code back.
Result: