The TypeScript language builds on JavaScript by adding syntax for type declarations and annotations. TypeScript uses static type-checking which provides ahead-of-time (AOT) compilation to help us identify errors before we run our code.
TypeScript is an open-source language with a huge developer community that continues to update its features, in a sequence of releases, making developer life easier. We’ll look into what the latest release, 4.1, has to offer. We’ll also explore the 4.2 beta release notes, which tease some new features for upcoming releases, and the future release roadmap.
Typescript 4.1 Release Features
Microsoft released TypeScript 4.1, the current version, in November 2020, roughly three months after 4.0. To install TypeScript via npm, so we can explore its features, use this command:
npm install -D typescript
Now, let’s take a look at what 4.1 offers.
Template Literal Types
String literal types in TypeScript enable us to model functions and APIs that expect a set of specific strings. Template literals were added to JavaScript back in ES2015. They provide a convenient and readable way to insert values into strings. These strings are delimited by backticks that evaluate any expression surrounded by the following, inserting the result into the string.
${…}
const testName = "TypeScript";
console.log(`Hello, ${testName}!`); // prints "Hello, TypeScript!"
console.log(`I said, HELLO ${testName.toUpperCase()}!`); // print "I said, HELLO TYPESCRIPT!"
TypeScript 4.1 lets you use the template literal syntax in types as well as values. This means, if your type represents a single string literal — or, more likely, a union of multiple string literals — you can use a template literal type to create a new string literal type derived from the old one.
type Vehicles = "bike" | "car" | "bus";
type Make = "Company1" | "Company2" | "Company3";
type VehicleType = `${Make}-${Vehicles}`;
const myVehicle: VehicleType = 'Company1-bike';
console.log(myVehicle) // Prints Company1-bike
As the example above shows, the transformation is distributed over the union, applied to each member separately, then produces a union of the results. If you include multiple union types in the template literal, it produces every combination.
Template literal types are helpful for correcting programmers’ mistakes right at the time of compilation. For example, if you mistakenly misspelled a word, it throws a compile-time error or shows suggestions to correct it before you run.
As you can see, this is useful for building out patterns of string types. It can also be useful when combined with other type operators such as keyof.
interface Person {
name: string;
age: number;
location: string;
}
// Generates: "getName" | "getAge" | "getLocation"
type PersonDataAccessorNames = `get${Capitalize<keyof Person>}`;
Capitalize is one of the four TypeScript 4.1 helper types that adjust capitalization of the string’s starting character. These types are:
- Capitalize
- Uncapitalize
- Uppercase
- Lowercase
Key Remapping in Mapped Types
Mapped types can be combined with template literal types. Think about generating a type for accessors using the previous example:
type PersonDataAccessors = {
[K in keyof Person]: () => Person[K];
}
This creates a new type, with the original data members mapped to a function returning their type:
{
name: () => string;
age: () => number;
location: () => string;
}
Now, we need to map keys of the type to:
get${Capitalize<keyof Person>}
We could do this simply as:
[K in `get${Capitalize<keyof Person>}`]
But, we need to retain the original K, so we access PersonDataAccessors (K). Remapping with as allows us to map the left-hand side while still accessing the original key:
type PersonDataAccessors = {
[K in keyof Person as `get${Capitalize<K>}`]: () => Person[K];
}
This, in turn, creates the type:
{
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
Recursive Conditional Types
TypeScript’s type system cannot flatten and build up container types at arbitrary levels, like JavaScript’s .then method on Promise instances (where it unwraps each promise until it finds a value that’s not “promise-like” and passes that value to the callback).
TypeScript 4.1 eases some restrictions on conditional types so they can model these patterns. In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.
For example, if we want to write a type to get the element types of nested arrays, we can write the following deepFlatten type.
type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;
function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
throw "not implemented";
}
// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);
Similarly, in TypeScript 4.1 we can write an Awaited type to deeply unwrap Promises.
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
p: Promise<T>,
onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;
Use these recursive types responsibly on resource-intensive executions as they can hit an internal recursion depth limit on complex inputs. When they hit that recursion limit, it triggers a compile-time error. In general, it’s better not to use these types than to write code that fails on more realistic examples.
Checked Indexed Accesses (--noUncheckedIndexedAccess)
Index signatures in TypeScript enable you to access arbitrarily-named properties, as demonstrated in the following Options interface. Here, we see that an accessed property that does not have the name path or the name permissions should have the type string | number:
interface Options {
path: string;
permissions: number;
// Extra properties are caught by this index signature.
[propName: string]: string | number;
}
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// These are all allowed too!
// They have the type 'string | number'.
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
}
A new flag, --noUncheckedIndexedAccess, provides a node where every property access (like opts.path) or indexed access (like opts "blabla") is considered potentially undefined. That means that if you need to access a property, like opts.path in the last example, you must check for its existence or use a non-null assertion operator (the postfix ! character):
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// These are not allowed with noUncheckedIndexedAccess
opts.yadda.toString();
Object is possibly 'undefined'.
opts["foo bar baz"].toString();
Object is possibly 'undefined'.
opts[Math.random()].toString();
Object is possibly 'undefined'.
// Checking if it's really there first.
if (opts.yadda) {
console.log(opts.yadda.toString());
}
// Basically saying "trust me I know what I'm doing"
// with the '!' non-null assertion operator.
opts.yadda!.toString();
}
This flag can be handy for catching out-of-bounds errors, but it might be noisy for a lot of code, so it is not automatically enabled by the --strict flag. However, if this feature is interesting to you, you should try it and determine if it makes sense for your team’s codebase.
TypeScript 4.2 Beta
TypeScript released its 4.2 beta version in January 2021. To install the latest TypeScript beta version, so you can check out its new features, use the command:
npm install typescript@beta
Let’s see what’s in store for TypeScript 4.2 beta.
Leading and Middle Rest Elements in Tuple Types
In previous versions of TypeScript, we could not write rest elements anywhere in tuple type except at the last position.
// A tuple with a *rest element* - holds at least 2 strings at the front,
// and any number of booleans at the back.
let e: [string, string, ...boolean[]];
TypeScript 4.2 allows rest elements anywhere within a tuple with only a few restrictions. For example, you can place a rest element anywhere in a tuple, so long as it’s not followed by another optional element or rest element. In other words, only one rest element per tuple, and no optional elements after rest elements.
interface Clown { /*...*/ }
interface Joker { /*...*/ }
let bar: [boolean, ...string[], boolean];
//This is a valid code
let StealersWheel: [...Clown[], "me", ...Joker[]];
// ~~~~~~~~~~ Error!
// A rest element cannot follow another rest element.
let StringssAndMaybeBoolean: [...string[], boolean?];
// ~~~~~~~~ Error!
// An optional element cannot follow a rest element.
Smarter Type Alias Preservation
In earlier versions, we were not able to see the declared type when we created a method with primitive types. For example, for the code below, when we hover over the doStuff function, we expect to see the type ‘BasicPrimitive | undefined’.
export type BasicPrimitive = number | string | boolean;
// Hover on 'doStuff' below to see what TypeScript displays for the return type.
export function doStuff(value: BasicPrimitive) {
if (Math.random() < 0.5) {
return undefined;
}
return value;
}
But, instead, we see the type as:
This is changed in TypeScript 4.2 to show what we'd expect:
Stricter Checks for the in Operator
In JavaScript, using a non-object type on the right side of the in operator triggers a runtime error. TypeScript 4.2 catches this error at design time.
"foo" in 42
//~~
// error! The right-hand side of an 'in' expression must not be a primitive.
--noPropertyAccessFromIndexSignature
TypeScript makes it possible to use “dotted” property access syntax, like user.name, when a type has a string index signature.
interface Options {
/** File patterns to be excluded. */
exclude?: string[];
/**
* It handles any extra properties that we haven't declared as type 'any'.
*/
[x: string]: any;
}
function processOptions(opts: Options) {
// In TypeScript 4.2 this is totally valid.
for (const excludePattern of opts.excludes) {
// ...
}
}
You can use the flag --noPropertyAccessFromIndexSignature to shift to TypeScript’s older behavior that issues an error.
noImplicitAny Errors Apply to Loose yield Expressions
When a yield expression is captured, but isn’t contextually typed (that is, TypeScript can’t figure out what the type is), TypeScript now issues an implicit any error.
function* g1() {
const value = yield 1; // report implicit any error
}
function* g2() {
yield 1; // result is unused, no error
}
function* g3() {
const value: string = yield 1; // result is contextually typed by type annotation of `value`, no error.
}
function* g3(): Generator<number, void, string> {
const value = yield 1; // result is contextually typed by return-type annotation of `g3`, no error.
}
TypeScript’s lift Callback in visitNode Uses a Different Type
TypeScript’s visitNode function takes a lift function. lift now expects a readonly Node instead of a NodeArray Node. This is considered an API-breaking change, and you can read more about it on GitHub.
Roadmap and Future Development Plans for TypeScript 4.3
In addition to the features discussed for 4.3 beta, TypeScript also teased its possible upcoming features in their official roadmap. Let’s look at some of the notable features.
typeof class Changes
typeof should allow users to describe types with class-specific characteristics. For example, take a look at the below class declaration.
var Thing: {
new () => {
instanceField: number;
}
staticField: string;
}
This is an awkward static and instance pattern that users need to write on anonymous object types and interface types. Instead, you can modify this, using typeof:
var Thing: typeof class Thing {
static staticField: string;
instanceField: number;
constructor() {}
}
Allow More Code Before super Calls in Subclasses
The proposed change relaxes the rule that super call must be the first statement within the subclass constructor. It allows something like the code below without throwing any errors.
class Base { }
class Derived extends Base {
public prop = true;
constructor(public paramProp = true) {
console.log("Hello, world!");
super();
}
}
More possible TypeScript 4.3 features include:
- Generalized index signatures
- --noImplicitOverride and the override keyword
- static Index Signatures
- Use unknown as the type for catch clause variables
Microsoft might still update these features as developers report more issues or submit more feature requests.
Beyond 4.3
TypeScript’s awesome open-source community is constantly updating it with new features. If you are a developer working with TypeScript and come across something you think can be better, you can start contributing bug fixes and new features to their GitHub page. The community might add them to forthcoming releases, or you can add them as a pull request.
The TypeScript roadmap also teases some possible features and investigations beyond version 4.3:
- Investigate nominal typing support
- Flatten declarations
- Implement the ES decorator proposal
- Investigate ambient, deprecated, and conditional decorators
- Investigate partial type argument inference
- Implement a quick fix to scaffold local @types packages
- Investigate error messages in haiku or iambic pentameter
- Implement decorators for function expressions and arrow functions
GrapeCity supports TypeScript in widgets such as SpreadJS. To learn more about how GrapeCity’s developer tools help you save time while incorporating spreadsheets, presentations, user interface controls, and other handy tools into your app, visit GrapeCity.com today.