TypeScript Features You Should No Longer Use
When TypeScript was originally invented by Microsoft in 2010, they implemented features missing from JavaScript at the time including module systems, classes, enums and decorators.
But in 2015 JavaScript added some of these features into the ECMAScript standard when ES6 was standardised.
TypeScript since then changed its governing principle to adapt to these changes and focus more on the type space and leave JavaScript to govern the runtime.
There are still a few remaining TypeScript features before this decision and they don’t fit the pattern of the rest of the language. To keep the relationship between TypeScript and JavaScript as clear as possible we should avoid these features.
Decorators
Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators can be used in five ways:
- class declaration
- property
- method
- parameter
- accessor
Class Decorator
When a class is decorated you have to be careful with inheritance because its descendants will not inherit the decorators. Let’s freeze the class to prevent inheritance completely.
@Freeze
class Person {}
function Freeze(constructor: Function) {
Object.freeze(constructor);
Object.freeze(constructor.prototype);
}
console.log(Object.isFrozen(Person)); // true
class Cat extends Person {} // error, cannot be extended
Class Decorator
Property decorators can be used for listening to state changes in a class.
Let’s override the name property to surround it in emojis. This allows us to set a regular string value, but run additional code on get/set as middleware, if you will.
export class Person {
@Emoji()
name = 'Sean';
}
// Property Decorator
function Emoji() {
return function(target: Object, key: string | symbol) {
let val = target[key];
const getter = () => {
return val;
};
const setter = (next) => {
console.log('updating name...');
val = `${next}`;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
Method Decorator
Method decorators allow us to override a method’s function, change its control flow, and execute additional code before/after it runs.
The following decorator will show a confirm message in the browser before executing the method. If the user clicks cancel, it will be bypassed. Notice how we have two decorators stacked below - they will be applied from top to bottom.
export class Person {
names = [];
@Confirmable('Are you sure?')
@Confirmable('Are you super, super sure? There is no going back!')
addName(name) {
this.names.push(name);
}
}
// Method Decorator
function Confirmable(message: string) {
return function (target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function( ... args: any[]) {
const allow = confirm(message);
if (allow) {
const result = original.apply(this, args);
return result;
} else {
return null;
}
};
return descriptor;
};
}
Originally added to support the Angular framework and it also requires the experimentalDecorators property to be set in tsconfig.json.
Their implementation has not yet been standardised by TC39, so any code you write today using decorators is liable to break or become non-standard in the future. Unless you’re using Angular or another framework that requires annotations and until they’re standardised, don’t use TypeScript’s decorators.
Enums
Enums allow a developer to define a set of named constants, instead of using numbers to avoid mistakes. For example, instead of using numbers like 1, 2 and 3, named constants like MONDAY, TUESDAY and WEDNESDAY can be used. This way the programmer doesn't have to remember which number to use.
Many programming languages use this concept. TypeScript also adds them to JavaScript:
enum WeekDay {
MONDAY = 0,
TUESDAY = 1,
WEDNESDAY = 2,
THURSDAY = 3,
FRIDAY = 4,
SATURDAY = 5,
SUNDAY = 6
}
const Wednesday = WeekDay.WEDNESDAY;
But with TypeScript, enums have some quirks. There are actually several variants on enums that all have subtly different behaviours.
Before looking into these behaviours, let's get familiarised with a term called substitution.
Substitution
For performance and code size reasons, it's often preferable to have a reference to an enum member replaced by its numeric equivalent when compiled:
const enum Weekday { THURSDAY = 4 }
const Thursday = Weekday.THURSDAY; // emitted as "var Thursday = 4;"
Sometimes you will not want enum members to be inlined, for example, because the enum value might change in a future version of the API.
Now let's look at the different variants of enums available in TypeScript.
const
An enum declaration can have the const
modifier. If an enum is const
, all references to its members are inlined.
const enum Weekday { THURSDAY = 4 }
const Thursday = Weekday.THURSDAY; // emitted as "var Thursday = 4;", always
const enums do not produce a lookup object when compiled. That means const enums go away completely at runtime. For this reason, it is an error to reference Weekday
in the above code except as part of a member reference. No Weekday
object will be present at runtime.
const with -preserveConstEnums flag
This flag has exactly one effect: non-declare const enums will emit a lookup object. Substitution is not affected. This is useful for debugging.
number
A number-valued enum like Weekday
. This is not safe as not any number can be assigned to this. It was originally designed to make bit flag structures possible.
string
A string-valued enum. This offers type safety, and also more transparent values at runtime. But unlike every other type in TypeScript, it is not structurally typed.
Since every other type in TypeScript uses structural typing for assignability, nominally typed string-valued enums come as a surprise:
enum WeekDay {
MONDAY = 'Monday',
TUESDAY = 'Tuesday',
WEDNESDAY = 'Wednesday',
THURSDAY = 'Thursday',
FRIDAY = 'Friday',
SATURDAY = 'Saturday',
SUNDAY = 'Sunday'
}
let Wednesday = WeekDay.WEDNESDAY; // Type is WeekDay
Wednesday = 'Wednesday'; // Error: Type '"Wednesday"' is not assignable to type 'WeekDay'.
This could course issues if you are exporting this as a module.
Let's assume you have a function that takes this enum as a parameter.
function assignDayOfWeek(weekday: WeekDay) {
...
}
If we are using this library inside a TypeScript project, we will have to import the enum and use that instead of passing in the day of the week as a string:
import { WeekDay } from 'days';
assignDayOfWeek('Wednesday'); // Error!
assignDayOfWeek(WeekDay.Wednesday);
But if the user is only using JavaScript, this can be used directly:
assignDayOfWeek('Wednesday'); // Ok!
This inconsistency is one reason we should avoid using string-valued enums. The other reason is the amount of code emitted by the TypeScript compiler:
"use strict";
var WeekDay;
(function (WeekDay) {
WeekDay["MONDAY"] = "Monday";
WeekDay["TUESDAY"] = "Tuesday";
WeekDay["WEDNESDAY"] = "Wednesday";
WeekDay["THURSDAY"] = "Thursday";
WeekDay["FRIDAY"] = "Friday";
WeekDay["SATURDAY"] = "Saturday";
WeekDay["SUNDAY"] = "Sunday";
})(WeekDay || (WeekDay = {}));
Instead, if you chose an alternative to enums that offered by TypeScript: a union of literal types.
type WeekDay = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday';
This offers has the advantage of translating more directly to JavaScript and as much safety as the enum. It also offers similarly strong autocomplete in your editor:
function assignDayOfWeek(weekday: WeekDay) {
if (weekday === 'W) // Autocomplete here suggests 'Wednesday'
}
And once compiled the type will go away, reducing the bundle size.
Read more about string literal types versus enums here: https://szaranger.medium.com/whats-an-enum-65db967d8bb6
Parameter Properties
Parameter properties let us create and initialise member variables in one place. It is a shorthand for creating member variables.
So far we have been doing this:
class Person{
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
This can be replaced with parameter properties like this:
class Person {
constructor(private name: string, private age: number) {
}
}
Usually TypeScript compilation erases types at compilation. But parameter properties would generate code after compilation. Here is the generated code:
"use strict";
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
This could be confusing to the reader of the code because the parameter is only used in generated code, but the source looks like it has unused parameters.
class Person {
constructor(private name: string, private age: number) {
// what happens to the parameters here ?
}
}
Mixing up parameter and non-parameter properties appear to be hiding the design of your classes.
For example:
class Person {
number: string;
street: string;
constructor(address: string) {
[this.number, this.street] = address.split(',');
}
}
There are three properties here: number, street and address, but it's hard to identify as there are only two properties listed before the constructor.
Because of this, it is recommended to avoid hiding the design of your class by using a mix of parameter and non-parameter properties.
Namespaces
If you are coming from languages like C# or C++ this seems like the most natural way to go. TypeScript used namespaces before ECMAScript 2015 because JavaScript didn't have a standard module system.
namespace Person {
function run() {}
}
But since ECMAScript 2015, namespaces have become a legacy feature. It is also not a part of the ECMAScript. And the TypeScript team will continue to follow the standard. There has been no improvement regarding TypeScript namespaces since the release of the ECMAScript modules standard in July 2014.
Triple-Slash Imports
To resolve module imports, before ES2015 TypeScript used a module keyword and triple-slash imports.
namespace Person {
function getName() {}
}
/// <reference path="Person.ts"/>
Person.getName();
This is no longer the standard of importing modules in TypeScript anymore. You should always use ES2015 style imports and exports.
Summary
Avoid following TypeScript features as much as you can:
- Decorators
- Enums
- Parameter properties
- Namespaces
- Triple-Slash Imports