In many ways, TypeScript is more like a powerful linting and documentation tool to author better JavaScript, rather than a separate programming language.
One significant benefit of TypeScript is its deliberate support for some of the latest ECMAScript language features. Updating to new versions of TypeScript provides support for new language features but in a safe, backwards-compatible manner. But aside from keeping up JavaScript, TypeScript regularly offers improvements to the actual experience of writing TypeScript. This includes tools to assist in refactoring, tools for finding references, renaming, and more.
Here we’ll explore not a complete, exhaustive list of everything that’s new in TypeScript over the past year, but instead some of the most exciting recent additions to TypeScript. For a more complete list of new features in each version, check out the TypeScript Release Notes.
“Immutable” objects and arrays
For marking array variables and parameters as being immutable at compile time, TypeScript provides the Readonly
and ReadonlyArray
helper types. However, using these helpers can feel a bit inconsistent with how types typically get annotated, especially when typing arrays using the []
characters after a type. TypeScript version 3.4 added a new way to mark parameters as being a readonly array and a new way to mark variable declarations as being immutable.
Improved UX for readonly array parameters
Parameters to a function that should get treated as immutable arrays can now also utilize the readonly
keyword. In the following example, the two method signatures are identical.
function foo(s: ReadonlyArray<string>) { /* ... */ }
function foo(s: readonly string[]) { /* ... */ }
In both cases, any attempt to modify the array (e.g. using the push
method) will result in an error. This change eliminates the need to use a generic helper type in one instance, which can lead to easier-to-read code. Object types can also get marked as readonly, but they still need to use the Readonly
helper type.
Improved UX for immutable variables with const assertions
Any variable declared with const
will not allow for its type to get changed. This is a concept that exists in JavaScript and that TypeScript adopts to narrow a type definition. But when working with non-primitive data types such as objects or arrays, those structures are not truly immutable. Using const
means that the specific instance of the object or array will remain the same, but the contents within can get changed quite easily. We can use the array’s push method to add a new value or we can change the value of a property on an object without violating the const
contract.
Using Readonly
and ReadonlyArray
we can indicate to TypeScript that it should treat the non-primitive as if it were truly immutable and throw an error anytime the code attempts a mutation.
interface Person {
name: string;
}
const person = {
name: 'Will'
} as Readonly<Person>;
person.name = 'Diana'; // error!
TypeScript 3.4 also introduces the concept of a const assertion, a simplified method of marking an object or array as being a constant, immutable value. This is done by adding an as const
assertion to the end of a variable declaration. This also has the added benefit of not needing to explicitly declare the type alongside the const assertion.
const person = {
name: 'Will'
} as const;
person.name = 'Diana'; // error!
// Arrays can be marked as const as well
const array = [1, 2, 3] as const;
array.push(4); // error!
The Omit helper type
TypeScript ships with several helper types that make it easy to map existing types to new types or conditionally set a type based on other types.
The Partial
helper marks all properties on an object as being optional. Prior to TypeScript 3.5, there was one type I found myself repeatedly adding to projects, the Omit
type. Just like the name states, Omit takes a type and a union of keys to omit from that type, returning a new type with those keys omitted. Gone are the days of remembering the correct incantation of Pick
and Exclude
to manually create Omit myself.
// now included in TypeScript 3.5
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface A {
propA?: string;
propB?: string;
propC?: string;
}
type B = Omit<A, 'propA' | 'propC'>;
const b: B = { propA: 'hi' }; // error;
New JavaScript features supported by TypeScript
When proposals for JavaScript reach stage 4 they are considered to be part of the next version of the language. However, this doesn’t mean that these new features can be used immediately as support for them must be built into all target environments and then the feature must exist in all versions that an application needs to support.
TypeScript’s compiler adds support for new JavaScript features and for many, can rewrite the code into a backwards-compatible format that can be used by all browsers supporting the build target set in an application’s tsconfig.json
.
Nullish coalescing
JavaScript developers are familiar with the concept of truthy and falsy. When checking for truthiness, there are 6 values that are always falsy: 0
, null
, undefined
, ""
, NaN
, and of course, false
. Most of the time we just want to know if a value is falsy but there are certain instances where you might actually want to know if the value was truly null
or undefined
. For example, if the code needs to know the difference between 0
and an undefined
value.
// using || won't work when index is 0
const getValueOrOne = (x?: number) => index || 1;
getValueOrOne(0); // 1 <-- Problematic
This code will work and set x to the value of index in all cases except where index = 0
. To write this correctly requires a more convoluted check of the values actual type.
// this works but is more convoluted
const getValueOrOne = (x?: number) => index !== null && index !== undefined ? : 1;
getValueOrOne(0); // 0
The code now works correctly but requires a more complex check. The new nullish coalescing operator (??
) simplifies this check by returning the value on the left side if it’s not null
or undefined
, otherwise it returns the value on the right side.
// this works!
const getValueOrOne = (x?: number) => index ?? 1;
getValueOrOne(0); // 0
getValueOrOne(2); // 2
getValueOrOne(); // 1
Optional chaining
Another new JavaScript feature available in TypeScript 3.7 is the optional chaining operator (?.
). I was first introduced to this as a language operator in the Groovy programming language and ever since I’ve wanted it in JavaScript. This operator allows for deep property access without the need to check that a value exists at every level. If at any point it encounters an undefined
value, it simply returns undefined
without throwing a TypeError
.
// without optional chaining
const value = foo && foo.bar && foo.bar.baz;
// with optional chaining
const value = foo?.bar?.baz;
Optional chaining gets even more powerful when combined with the nullish coalescing operator, allowing for setting a value to a deeply nested value or a default value in the case that it doesn’t exist.
const value = foo?.bar?.baz ?? 'default value';
Private fields
TypeScript has had its own concept of private
class fields since its inception, before classes were defined in the JavaScript standard. But TypeScript’s private
is a compile-time private, meaning the compiler will throw errors if a private method or property is accessed outside of its class methods. JavaScript now includes the ability to mark a property or method as private to a class, though its private is semantically and syntactically different.
JavaScript private fields do not use the private
keyword. Instead, they start with #
.
class Fan {
#on = false;
private name = 'fan';
turnOn() {
this.#on = true;
}
isTurnedOn() {
return this.#on;
}
}
const fan = new Fan();
fan.isTurnedOn(); // false
fan.turnOn();
fan.isTurnedOn(); // true
fan.on; // does not exist
fan.#on; // not accessible
fan.name; // compile-time error, but accessible in JS
Currently, private fields are supported, with private methods being a Stage 3 proposal. Currently private and #private fields cannot get used together. Both approaches are useful and it remains a choice for the developer to determine which is required to solve the problem.
Top-level await
Asynchronous programming has greatly improved in JavaScript and TypeScript, first with the introduction of promises and then with the async/await syntax to cleanly author asynchronous code.
One case where you need to use promise callbacks rather than async/await is calling an asynchronous method from outside of an asynchronous function, such as in the top-level of a module or application. One workaround for this has been to create an asynchronous immediately invoked function expression (IIFE) and perform the asynchronous calls inside.
(async () => {
const response = await fetch('https://api.github.com/users/sitepen');
const data = await response.json();
console.log(`Check out the blog at ${data.blog}`);
})();
TypeScript now supports the top-level await feature from JavaScript letting you use the await
keyword outside of an async
function, specifically in the top-level of a module script. This is wonderful for keeping code concise and to the point. However, one criticism of top-level await is that it can lead to bottlenecks in module loading where one module could slow down the loading of the application as it waits for promises to get resolved before the module gets resolved.
const response = await fetch('https://api.github.com/users/sitepen');
const data = await response.json();
export default { ...data };
An improved TypeScript Playground
This isn’t really a new feature of TypeScript, but given that we’re treating TypeScript as a tool in this post, the TypeScript Playground is an effective tool to quickly try out theories on types while simultaneously viewing the generated JavaScript. Most of the examples in this post were tested in the TypeScript playground, which now includes the ability to run a specific version of TypeScript (including nightly) and contains several examples to help anyone interactively get started with the language.
What are you excited about?
TypeScript is a tool that helps us write better, more expressive JavaScript. Its tooling keeps us honest and makes tasks such as renaming and refactoring trivial where they would normally be extremely tedious in plain JavaScript. Adding helpers like Omit
, const assertions, and continuously improving support for complex types on top of introducing the latest features coming to JavaScript are why many consider TypeScript to be their preferred tool, language, and ecosystem. What features are you excited about?
Need help architecting or creating your next TypeScript application or determining if TypeScript is the right approach for you? Contact us to discuss how we can help!