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!