6 useful TypeScript 3 features you need to know
TypeScript releases are coming thick and fast, so it’s easy to miss some of the handy features that are added. Here are some of the new features that I’ve found useful in the last year.
The unknown type
When dealing with data from a 3rd party library or web API that hasn’t got TypeScript types, we often reach for the any
type. However, this means no type checking will occur on the data.
const person: any = await getPerson(id);
const postcode = person.address.postcode; // TypeScript doesn't yell at us but what if there is no address property ... 💥!
The unknown
type is a strongly-typed alternative to any
in these situations:
const person: unknown = await getPerson(id);
const postcode = person.address.postcode; // 💥 - Type error - Object is of type 'unknown'
The unknown
type forces us to do explicit type checks when interacting with it:
const person: unknown = await getPerson(id);
if (isPersonWithAddress(person)) { const postcode = person.address.postcode;
}
function isPersonWithAddress(person: any): person is Person { return "address" in person && "postcode" in person.address;}
Readonly arrays
We may think the following would raise a type error:
type Person = {
readonly name: string; readonly scores: number[];};
const bob: Person = {
name: "Bob",
scores: [50, 45]
};
bob.scores.push(60); // does this raise a type error?
TypeScript is perfectly happy with this, though. The readonly
keyword before an array property name only ensures the property can’t be set to a different array - the array itself isn’t immutable.
However, we can now put an additional readonly
keyword before the array itself to make it immutable:
type Person = {
readonly name: string;
readonly scores: readonly number[];};
const bob: Person = {
name: "Bob",
scores: [50, 45]
};
bob.scores.push(60); // 💥 - Type error - Property 'push' does not exist on type 'readonly number[]'
There is also a ReadonlyArray
generic type that does the same thing:
type Person = {
readonly name: string;
readonly scores: ReadonlyArray<number>; // same as readonly number[]};
const assertions
const
assertions help us create immutable structures:
function createGetPersonAction() {
return { type: "GetPerson" } as const;}
const getPersonAction = createGetPersonAction(); // `getPersonAction` is of type `{ readonly type: "GetPerson"; }`
Here’s another example, this time using the alternative angle bracket syntax for the const
assertion:
type Person = {
id: number;
name: string;
scores: number[];
};
const people: Person[] = [
{ id: 1, name: "Bob", scores: [50, 45] },
{ id: 2, name: "Jane", scores: [70, 60] },
{ id: 3, name: "Paul", scores: [40, 75] }
];
function getPersonScores(id: number) {
const person = people.filter(person => person.id === id)[0];
return <const>[...person.scores];}
const scores = getPersonScores(1); // `scores` is of type `readonly number[]`
scores.push(50); // 💥 - Type error - Property 'push' does not exist on type 'readonly number[]'
const
assertions also prevent literal types from being widened. The example below raises a type error because Status.Loading
is widened to a string:
function logStatus(status: "LOADING" | "LOADED") {
console.log(status);
}
const Status = {
Loading: "LOADING",
Loaded: "LOADED"
};
logStatus(Status.Loading); // 💥 - Type error - Argument of type 'string' is not assignable to parameter of type '"LOADING" | "LOADED"'
Using a const
assertion will result in Status.Loading
maintaining its type of "LOADING"
:
const Status = {
Loading: "LOADING",
Loaded: "LOADED"
} as const;
logStatus(Status.Loading); // Type is "LOADING"
Marius Schulz has an in depth post on const
assertions.
Optional chaining
Optional chaining allows us to deal with object graphs where properties may be null
or undefined
in an elegant manner.
Consider the following example:
type Person = {
id: number;
name: string;
address?: Address;
};
type Address = {
line1: string;
line2: string;
line3: string;
zipcode: string;
};
function getPersonPostcode(id: number) {
const person: Person = getPerson(id);
return person.address.zipcode; // 💥 - Type error - Object is possibly 'undefined'
}
The code raises a type error because the address
property is optional and, therefore, can be undefined
. The optional chaining operator (?
) allows us to navigate to the zipcode
property without generating any type errors or runtime errors:
function getPersonPostcode(id: number) {
const person: Person = getPerson(id);
return person.address?.zipcode; // returns `undefined` if `address` is `undefined`}
If the address
property is undefined
at runtime, then undefined
will be returned by the function.
Nullish coalescing
Nullish coalescing allows us to substitute a different value for a value that is null
or undefined
.
Consider the following example:
type Person = {
id: number;
name: string;
score?: number;
};
function getPersonScore(id: number): number {
const person: Person = getPerson(id);
return person.score; // 💥 - Type error - Type 'number | undefined' is not assignable to type 'number'
}
The code raises a type error because the score
property is optional and, therefore, can be undefined
. The nullish coalescing operator (??
) allows a different value (specified in the right-hand operand) to be used when the left-hand operand is null
or undefined
:
function getPersonScore(id: number): number {
const person: Person = getPerson(id);
return person.score ?? 0;}
The nullish coalescing operator is more robust than using person.score || 0
because 0
will be returned for any falsy value rather than just null
or undefined
.
The Omit utility type
The Omit
utility type allows a new type to be created from an existing type with some properties removed.
Consider the following example:
type Person = {
id: number;
name: string;
mobile: string;
email: string;
};
If we want to create a Person
type without any of the contact details we can use the Omit
utility type as follows:
type PersonWithoutContactDetails = Omit<Person, "mobile" | "email">; // { id: number; name: string; }
The benefit of this is that the PersonWithoutContactDetails
type will change if Person
changes without us having to touch PersonWithoutContactDetails
:
type Person = {
id: number;
name: string;
mobile: string;
email: string;
age: number;
};
// PersonWithoutContactDetails automatically becomes { id: number; name: string; age: number;}
If you to learn more about TypeScript, you may find my free TypeScript course useful: