DEV Community

Cover image for 6 useful TypeScript 3 features you need to know
Carl
Carl

Posted on

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;}

Originally published at https://www.carlrippon.com/6-useful-typescript-3-features-you-need-to-know/ on Dec 18, 2019.

Top comments (0)