Recently TypeScript is increasingly gaining popularity and companies like Slack are praising their move to TypeScript. But with TypeScript shipping monthly updates to the language and its tools even a regular TypeScript developer might lose track of what is being added.
Here is a list of 10 of my favorite recent additions to the language since version 2.0:
1. tsc --init
This is a great feature if you want to start off a new TypeScript project. If you run in a folder:
tsc --init
TypeScript will automatically create a tsconfig.json
in your directory. The neat thing is that it automatically populates it will all available options (some of them commented out) and a description of the respective option. This is especially useful if you are new to TypeScript and want to get an overview of all options.
If you just want an overview of all available options you can also try out this command:
tsc --help --all
2. extends
in tsconfig.json
Have you found yourself in the situation where you needed two separate tsconfig.json
files for example for tests but most of the options were shared? With the extends
option we can specify a configuration file that we want to base our current file on.
As an example lets say we typically compile our code with a target of esnext
:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"strict": true
}
}
But we want to create a separate version that is es5
compatible. We can extend from the new config from the existing one:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es5",
"outDir": "./es5"
}
}
If we now run the compiler with this file it will take the flags from the tsconfig.json
and override the target
with es5
and add the outDir
option. You can specify a certain config file in the TypeScript compiler with the -p
option:
tsc -p tsconfig-es5.json
3. type
vs interface
Typically you type objects in two different ways in TypeScript. Either by using type
or by using interface
:
type MyObjectUsingType = {
foo: string;
bar: number;
};
const option1: MyObjectUsingType = {
foo: 'hello',
bar: 42,
};
interface MyObjectUsingInterface {
foo: string;
bar: number;
}
const option2: MyObjectUsingInterface = {
foo: 'hello',
bar: 42,
};
On the first glimpse there is not really a difference except of some minor syntax differences. However, interface
has a few benefits. Namely that we can both extend
as well as implement
it:
interface DrivedFromInterface extends MyObjectUsingInterface {
bla: boolean;
}
class MyClass implements MyObjectUsingInterface {
foo: string = 'some string';
bar: number = 43;
}
Addtionally you will be able to redefine an interface multiple times which means you will be able to add addtional parameters at different parts in your code.
4. keyof
This is one of my favorite recent additions that was added version 2.1
. Let’s say we have a type of an interface Configuration
:
interface Configuration {
baseUrl: string;
ttl: number;
}
We want to write a set
method that retrieves a key and a value. With keyof
we can create “dynamically” a type that represents all possible keys of Configuration
:
type ConfigurationOption = keyof Configuration;
Now we can write a function that is typed and automatically ensures that both key
and value
have the correct types:
function set<T extends ConfigurationOption>(key: T, value: Configuration[T]) {
console.log(`Setting: ${key} to ${value}`);
}
set('baseUrl', 'https://moin.world');
set('ttl', 42);
set('baseUrl', true); // Invalid! Compiler catches this.
Now we have a function that will not only check that we are passing in a valid value for key
but also that the type of value
is the right one for the respective option.
5. Mapped Types
Now that we know about the power of keyof
let me show you one more awesome thing related to this. This is not limited to types generated with keyof
though.
Let’s say we have our types from earlier:
interface Configuration {
baseUrl: string;
ttl: number;
}
type ConfigurationOption = keyof Configuration;
We want to create a new type of a map that tracks when the respective value has last been updated. We basically need a new type that maps for every key of the Configuration
interface to a Date
object. We can do this with mapped types really easily:
type ConfigurationLastUpdated = { [O in ConfigurationOption]: Date };
const updated: ConfigurationLastUpdated = {
baseUrl: new Date(),
ttl: new Date(),
};
We can do the same with any other union-type as well:
type Color = 'red' | 'blue' | 'green';
type ColorToRgb = { [C in Color]?: string };
const rgbMap: ColorToRgb = {
red: '#ff0000',
};
6. Mixin classes
Mixin classes are an addition that came in TypeScript version 2.2
in February 2017. It’s a way for you to extend classes in a dynamical fashion. Let’s say we have a set of 2D classes:
class Point {
x: number = 0;
y: number = 0;
}
class Line {
x: number = 0;
y: number = 0;
length: number = 0;
}
Now we want to have an easy way to turn these classes into classes ready for the third dimension. This is what we can use mixin classes for. Before we can write one we first need to add a quick helper type though:
type Constructor<T> = new (...args: any[]) => T;
With this type given we can create a function that receives a Constructor
as a base and returns an anonymous class that extends from this base class and adds a z
property:
function ThirdDimension<T extends Constructor<{}>>(Base: T) {
return class extends Base {
z: number = 0;
constructor(...args: any[]) {
super(...args);
}
};
}
Given this function we can then easily create new classes based on our existing ones in a dynamic fashion:
const Point3D = ThirdDimension(Point);
const p = new Point3D();
p.z = -1;
7. object
vs Object
Object
has been around as a type for quite a while but in 2.2
TypeScript introduced a new type object
. The difference in spelling is minimal but they represent completely different groups of objects.
Object
is basically anything that is not undefined
or null
. On the contrary object
represents anything that isn’t a primitive type (string
, number
, boolean
, etc.) plus undefined
.
The reason why this type exists is that there is a set of functions like Object.create
that get an object
as argument. With the introduction of object
as a type we can now properly type these function calls and catch mistakes early.
const foo: Object = 'foo';
const bar: object = 'bar'; // ==> This is invalid
const bla: object = new Date();
8. Transpiling async
/await
to ES5/ES3
If you are a fan of async
/await
but were bummed out that you had to support ES5 or even ES3 as your target, you will love this. TypeScript is able to transpile async
/await
to both ES5 and ES3 as long as you have a Promise
polyfill.
Simply add the respective libs to the lib
property in the tsconfig.json
and adjust the target
to your preferred target:
{
"compilerOptions": {
"module": "commonjs",
"target": "es3",
"lib": ["dom", "es2015", "es2015.promise"]
}
}
From that moment on you can use async
/await
simply in your code:
async function hello(): Promise<string> {
const x = await Promise.resolve('foo');
return x;
}
hello().then((x: string) => {
console.log(x);
});
9. String valued enums
This is a feature that has been long in the making. In fact some of the issues date back to 2014 and earlier. Now in TypeScript version 2.4
we are finally getting them: string valued enums 🎊
Using them is super straight forward:
enum Color {
Red = '#ff0000',
Green = '#00ff00',
Blue = '#0000ff',
}
const myFavoriteColor = Color.Green;
let chosenColor = Color.Red;
In this case myFavoriteColor
will be of the value '#00ff00'
during runtime and it has the type of Color.Green
since it’s a constant. For the chosenColor
, TypeScript will automatically walk one step up and assign it the type Color
since it could change over time. The value assignned during runtime will be '#ff0000'
as expected.
The important part is that for string valued enums you will need an initializer while numeric ones can be inferred. However, this allows you to also mix the two:
enum ErrorCodes {
NotFound = 404,
MethodNotAllowed,
Unknown = 'unknown',
}
console.log(ErrorCodes.NotFound === 404);
console.log(ErrorCodes.MethodNotAllowed === 404);
console.log(ErrorCodes.Unknown === 'unknown');
In this case we specify a numeric value for ErrorCodes.NotFound
of 404
and because ErrorCodes.MethodNotAllowed
follows in the listing, it will automatically be assigned 405
. ErrorCodes.Unknown
, however, is assigned to a string value of 'unknown'
. This is only possible due to the strict enforcing of having an initializer for string values.
10. allowJs
, checkJs
, // @ts-check
These three options are very exciting if you are new to TypeScript or if you have existing JavaScript.
If you pass the --allowJs
flag to the TypeScript compiler or if you enable it in the tsconfig.json
file, TypeScript will automatically pick up your JavaScript files to compile them. This means you can use this feature for example to transpile your ES2015 code to ES5 without the use of a tool like Babel.
TypeScript won’t perform any type checks on these files unless you pass the --checkJs
flag that was introduced in TypeScript version 2.3
. If you want to enable this only for specific files you can simply add a comment to the top of your file:
// @ts-check
console.log('I will be type checked');
Similarly you can disable it on selected files using // @ts-nocheck
of for a specific line by adding above the line // @ts-ignore
.
These features also work in combination with the respective editor integrations like in VS Code. It’s super useful if you want to move your code base over to TypeScript but you can’t change the actual files to TypeScript yet for whatever reason.
Summary
This is just a small overview of a few features that are currently in TypeScript (or are coming this month) that I’m excited about. But there is much more planned, including Variadic types, ambient decorators (decorators that are only there during compile time) but don’t affect runtime and much more. A great overview of what’s planned is the Roadmap in the TypeScript wiki.
I would love to hear what your experience with TypeScript is, why you do or do not like it and what your favorite TypeScript feature is. Simply shoot me a message on Twitter @dkundel.