Skip to content

Instantly share code, notes, and snippets.

@MarcelCutts
Created July 25, 2018 13:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarcelCutts/f90d14cfc11dcfae567375aac960617a to your computer and use it in GitHub Desktop.
Save MarcelCutts/f90d14cfc11dcfae567375aac960617a to your computer and use it in GitHub Desktop.
A list of findings after experimenting with TypeScript

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.

  1. Configuration
  2. Permissiveness
  3. Strict mode
  4. Lock-in
  5. Progressive inclusion

Configuration

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.

Permissiveness

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 or undefined
  • 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.

Strict mode

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.

Strict mode: Compiler forces inelegant code

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

Strict mode: Compiler error messages are unhelpful

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.

Strict mode: Unhelpful tooling

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.

Lock-in

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));

Progressive inclusion

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.

@gabro
Copy link

gabro commented Aug 21, 2018

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.

tsc --target es6 yourfile.ts

Result:

class Animal {
    constructor(name) {
        this.name = name;
    }
    move(distanceInMeters = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
class Snake extends Animal {
    constructor(name) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

@gabro
Copy link

gabro commented Aug 21, 2018

Re: Strict mode: Compiler forces inelegant code

That doesn't seem true.
I mean, it might be a quirk with TSLint (which you're not forced to use, it's a separate tool), but this compiles just fine in strict mode:

interface IFunction {
	(s: string): boolean;
}

interface IObject {
	message: string
	isMessage: IFunction
}

@gabro
Copy link

gabro commented Aug 21, 2018

Re: Progressive inclusion

It's not true that you need to convert everything to TS in one go.
We've progressively migrated a big codebase at work over the span of a few months.

I won't go into details here, but you can use the --allowJs to mix JS and TS files.

@gabro
Copy link

gabro commented Aug 21, 2018

compilation becomes a two step process and requires adding the TS compiler loader into babel

Not true either. TS can replace Babel entirely (unless you're using Babel plugins that you want to keep for some reason)

You can definitely make a two step build, if you need/want/like to, but that's optional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment