Categories
TypeScript

TypeScript Advanced Types: Union and Intersection

TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There’re multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at intersection and union types.

Intersection Types

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s denoted by an & sign. All members of all the types are required in the object of an intersection type.

For example, we can use the intersection type like in the following code:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
}

let employee: Animal & Person & Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20,
  employeeCode: '123'
}

As we can see from the code above, each type is separated by an & sign. Also, the employee object has all the properties of Animal , Person , and Employee . Each property has a type that’s defined in each interface. If the structure doesn’t match exactly, then we’ll get error messages like the following from the TypeScript compiler:

Type '{ kind: string; firstName: string; lastName: string; age: number; }' is not assignable to type 'Animal & Person & Employee'.

Property 'employeeCode' is missing in type '{ kind: string; firstName: string; lastName: string; age: number; }' but required in type 'Employee'.(2322)

input.ts(12, 3): 'employeeCode' is declared here.

The error above occurs if we have the following code:

let employee: Animal & Person & Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20
}

TypeScript looks for the employeeCode property since the employeeCode property is in the Employee interface.

If 2 types have the same member name but different type, then it’s automatically assigned the never type, when they’re joined together as an intersection type. This means that we can’t assign anything to it. For example, if we have:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
  age: string;
}

let employee: Animal & Person & Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20
}

Then we get the following error from the TypeScript compiler:

Type 'number' is not assignable to type 'never'.(2322)

input.ts(8, 3): The expected type comes from property 'age' which is declared here on type 'Animal & Person & Employee'

If we omit the property then the compiler will also raise an error about the age property being missing. Therefore, we should never have types that have the same member name if we want to create intersection types from them.

Union Types

Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol.

For example, we can define an object that has a union type like in the following code:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20
}

The code above has an employee object of the Animal | Person | Employee type which means that it can have some of the properties of the Animal, Person, or Employee interfaces. Not all of them have to be included, but if they’re included, then the type has to match the ones in the interface.

With union types, we can have 2 types that have the same member name but with different types. For example, if we have the following code:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
  age: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: '20'
}

Then we can assign both a number or a string to the age property. This fits with the dynamic nature of JavaScript while letting us assign data types to objects. This is different from traditional object-oriented code where we may abstract common members into a parent class and then derive sub-classes from it which has specialized members.

The pipe symbol means the object of a union type can take on none, some, or all properties of each type.

Accessing members in an object of a union type is different from how members are accessed from an intersection type. Objects that have intersection types have to have all the properties listed in the members of each type, so logically, we can access all the properties that are defined in the object. However, this isn’t the case with union types since some members may be available to only some of the types that make up the union type.

If we have a union type, then we can only access members that are available in all the types that form the union type. For example, if we have:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
  age: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: '20'
}

console.log(employee)

Then we can’t access any properties of the employee object since none of the members are available in all the types. If we try to access a property like the kind property, we’ll get the following error:

Property 'kind' does not exist on type 'Animal | Person | Employee'.

Property 'kind' does not exist on type 'Person'.(2339)

If we want to make some property accessible, we can write something like the following code to let us access a property:

interface Animal {
  kind: string;
}

interface Person {
  kind: string;
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  kind: string;
  employeeCode: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20,
  employeeCode: '123'
}

console.log(employee.kind)

In all 3 interfaces above, we have the kind member. Since they’re in all 3 interfaces, which we used in the union type, we can access the employee.kind property. Then we would get the text ‘human’ in the console.log statement.

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s formed by joining multiple types by an & sign. Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol. It lets us create a new type that has some of the structure of each type that forms the union type.

Categories
TypeScript

What’s New in TypeScript 4.0?

TypeScript 4.0 comes with lots of new features to make JavaScript development easier.

In this article, we’ll look at the best features of TypeScript 4.

Variadic Tuples

TypeScript 4.0 comes with data types for tuples with a variable number of elements.

We can use the spread operator to create a type with the elements we want in our tuple.

For example, we write:

type Strings = [string, string];
type Numbers = number[];

type Unbounded = [...Strings, ...Numbers, boolean];

to create an Unbounded data type to add a tuple type with strings, numbers, and booleans.

The inference process is also automatic so that if we have 2 strings, numbers, and a boolean in the same order, TypeScript will infer the tuple as having the Unbounded type.

Labeled Tuple Elements

We can label tuple elements.

For example, we can write:

type Range = [start: number, end: number];

to restrict args to have a string and a number.

We can also write:

type Foo = [first: number, second?: string, ...rest: any[]];

to have rest entries in our tuple.

If our tuple has type Foo , then the tuple starts with a number and a string.

Then the rest of the entries can be anything.

Labels don’t require us to name our variables differently when destructuring.

For example, if we have:

function foo(x: [first: string, second: number]) {
  const [a, b] = x;
}

then we can name then destructured variables anything we want.

Class Property Inference from Constructors

TypeScript 4.0 can infer class properties’ types from the constructor.

For example, if we have:

class Square {
  area;
  length;

  constructor(length: number) {
    this.length = length;
    this.area = length ** 2;
  }
}

then TypeScript 4.0 knows that this.length and this.area are numbers automatically.

If there’s a chance that the value of them are undefined , then the TypeScript compiler will notify us of that.

So if we have:

class Square {
  length;

  constructor(length: number) {
    if (Math.random()) {
      this.length = length;
    }
  }

  get area() {
    return this.length  ** 2;
  }
}

then we’ll know that this.length may be undefined .

We would need a type assertion even if we know that it’s always defined.

For example, we can write:

class Square {
  length!: number;

  constructor(length: number) {
    this.initialize(length);
  }

  initialize(length: number) {
    this.length = length;
  }

  get area() {
    return this.length ** 2;
  }
}

We set length to be non-null with the ! symbol and set its type explicitly to number to make sure this.length is always a number.

Short-Circuiting Assignment Operators

TypeScript 4.0 has new assignment operator shorthands.

Now we write the logical AND, logical OR, and bullish coalescing operators with shorthands.

For example, instead of writing:

a = a && b;
a = a || b;
a = a ?? b;

We write:

a &&= b;
a ||= b;
a ??= b;

unknown on catch Clause Bindings

We can specify the binding variable of the catch clause to have an unknown type instead of an any type.

With the unknown type, we have to cast the exception object explicitly before we can do things with it.

For example, we can write:

try {
  // ...
} catch (e: unknown) {
  if (typeof e === "string") {
    console.log(e.toUpperCase());
  }
}

Then we check if e is a string before we call toUpperCase on it.

Conclusion

TypeScript 4.0 comes with many new language features that we can use to check for types.

Type inference is also improved.

Categories
TypeScript

TypeScript 4.0 — Breaking Changes

TypeScript 4.0 comes with lots of new features to make JavaScript development easier.

In this article, we’ll look at the best features of TypeScript 4.

Custom JSX Factories

TypeScript 4.0 lets us customize the fragment factory with the jsxFragmentFactory option.

We can set the settings in tsconfig.json by writing:

{
  compilerOptions: {
    target: "esnext",
    module: "commonjs",
    jsx: "react",
    jsxFactory: "h",
    jsxFragmentFactory: "Fragment",
  },
}

Also, we can set the factories in a per-file basis:

/** @jsx h */
/** @jsxFrag Fragment */

jsxFactory is the function to render JSX into JavaScript.

And jsxFragmentFactory lets us render JSX fragments to JavaScript.

Speed Improvements in build mode with --noEmitOnError

The noEmitError option lets us compile incrementally by caching data in a .tsbuildinfo file.

This will give us a performance when building with the --incremental flag.

--incremental with --noEmit

The --noEmit flag can now be used with the --incremental flag to speed up incremental builds.

Editor Improvements

With TypeScript 4.0, Visual Studio Code and Visual Studio becomes smarter.

One thing it can do is shrink long chains of undefined checks into the optional chaining or nullish coaslescing expressions.

For example, if can shrink:

a && a.b && a.b.c

into:

a?.b?.c

It also adds support for the /** @deprecated */ flag to mark code as being deprecated.

Then the code will appeared as being crossed out.

Partial Semantic Mode at Startup

Partial semantic mode speeds up the startup time of Visual Studio Code and Visual Studio by partially parsing the code instead of parsing everything during startup.

Now the startup delay should only be a few seconds because of that.

This means that we’ll get the rich experience of the editors immediately.

Smarter Auto-Imports

Auto-imports is now smarter since it works with packages that ships with its own types.

TypeScript 4.0 can search for the packages listed in package.json for types so that it can find the types and let us do auto-import on those packages.

We set the typescript.preferences.includePackageJsonAutoImports to true to let us do the auto-imports.

Properties Overriding Accessors (and vice versa) is an Error

Properties that override accessors and vice versa is an error with TypeScript 4.0.

For example, if we have:

class Base {
  get foo() {
    return 100;
  }
  set foo(value) {
    // ...
  }
}

class Derived extends Base {
  foo = 10;
}

then we’ll get an error since we set this.foo to a new value.

Operands for delete must be optional

Operands for delete must be optional since they can be removed.

So if we have something like:

interface Thing {
  prop: string;
}

function f(x: Thing) {
  delete x.prop;
}

then we get an error because props is required with Thing .

Conclusion

There are some breaking changes that comes with TypeScript 4.0.

Also, it has more features for setting JSX factories and converting syntax automatically.

Categories
TypeScript Best Practices

TypeScript Antipatterns to Avoid

TypeScript is a language that extends the capabilities of JavaScript by adding type annotations to JavaScript code. This lets us avoid bugs from unexpected data types.

In this article, we’ll look at some antipatterns to avoid when writing TypeScript code.

Overusing the any Type

The point of using TypeScript is to have types in variables and functions. So we should use them wherever we can.

Therefore, we shouldn’t use the any type in most of our code.

Overusing Classes

If our TypeScript classes don’t have many methods, then we don’t need to define a class just to use it to type variables and functions.

Also, if we only have a single instance, then wrapping the logic within a class doesn’t make sense.

Instantiating classes introduce complex and it’s hard to optimize when minifying code.

Instead, we can define object literals and use interfaces or the typeof operator to get the type of an object.

For example, write the following interface for objects:

interface Person {
    firstName: string;
    lastName: string;
}

const person: Person = {
    firstName: 'Jane',
    lastName: 'Smith'
}

Then we can use them like we have above with person .

Also, if we don’t know the exact structure that the object will have, we can use typeof operator as follows:

const person = {
    firstName: 'Jane',
    lastName: 'Smith'
}

const person2: typeof person = {
    firstName: 'Joe',
    lastName: 'Smith'
}

Using typeof is a convenient way to define types without writing lots of code.

If our object has methods, we can do the same thing as above:

interface Person {
    firstName: string;
    lastName: string;
    fullName: (firstName: string, lastName: string) => string
}

const person: Person = {
    firstName: 'Jane',
    lastName: 'Smith',
    fullName(firstName: string, lastName: string) {
        return `${firstName} ${lastName}`;
    }
}

TypeScript will do type inference with typeof :

const person = {
    firstName: 'Jane',
    lastName: 'Smith',
    fullName(firstName: string, lastName: string) {
        return `${firstName} ${lastName}`;
    }
}
const person2: typeof person = {
    firstName: 'Joe',
    lastName: 'Smith',
    fullName(firstName, lastName) {
        return `${firstName} ${lastName}`;
    }
}

It’ll force us to add the fullName method to person2 if it’s missing, and it’ll force us to add the parameters and return a string.

Otherwise, we’ll get compiler errors.

Using the Function Type

The Function type is a generic type for functions. It’s like any for variables. We should specify the data types of parameters and return types in our functions.

Instead, we should add types to parameters and return types as follows:

type ArithmeticFn = (a: number, b: number) => number
const add: ArithmeticFn = (a: number, b: number): number => a + b;

We have the types in the type alias ArithmeticFn and also add . That’s enough type annotations in our code.

Also, once again, we can use typeof to do type inference:

const add = (a: number, b: number): number => a + b;
const subtract: typeof add = (a, b) => a-b

Then subtract also has the same parameter and return types as add .

Messing with Type Inference

We shouldn’t put useless type annotations with type inference will do the job.

For example, in the following example:

const courses = [{
    name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse: any = {...course};
newCourse.description = 'Great intro to TypeScript';
courses[0] = newCourse;

We have an extra any annotation that we shouldn’t have.

Instead, if we want to add a new property to an object after copying it, we can write the following:

const courses = [{
    name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse = {...course, description: 'Great intro to TypeScript'};
courses[0] = newCourse;

Then the TypeScript compiler won’t throw an error and we still get type inference from the TypeScript compiler since we didn’t use the any type.

Copying and Pasting Partial Type Definitions

Another thing that we shouldn’t do is copy and pasting partial type definitions from other places into our own code.

If we want to get the type of an object with an unknown type, we can use the typeof operator.

For example, we can write:

const person = {
    firstName: 'Joe',
    lastName: 'Smith',
    age: 20
}

const person2: typeof person = {
    firstName: 'Jane',
    lastName: 'Smith',
    age: 20
}

The TypeScript compiler will automatically recognize the type of person and will do checks when we define person2 to check if everything’s there.

Lookup Type

We can also use one property of an object as its own type. This is called a lookup type.

For example, we can write:

const person = {
    firstName: 'Joe',
    lastName: 'Smith',
    age: 20
}

const foo: typeof person.firstName = 'foo';

In the code above, TypeScript recognized person.firstName ‘s type is a string, so typeof person.firstName would be a string.

Mapped Types

We can create mapped types to map all properties of a type to something else.

For example, we can make all the properties of a type optional by writing:

interface Person{
    firstName: string;
    lastName: string;
}

type Optional<T> = {
    [P in keyof T]?: T[P];
};

const partialPerson: Optional<Person> = {};

The code above compiles and runs because we created a new type from Person where all the properties are optional.

Getting the Return Type of a Function

We can get the return type function with the ReturnType generic to pass in a type of the function. Then we’ll get the return type of that function.

For example, if we have:

const add = (a: number, b: number) => a + b;
const num: ReturnType<typeof add> = 1;

Then ReturnType<typeof add> will be number , so we have to assign a number to it.

TypeScript’s type inference is also working here. The ReturnType generic is available since TypeScript 2.8

Conclusion

In many cases, we can use the typeof operator, lookup types, or mapped types to annotate the types flexibly without losing type check capabilities.

This means we should eliminate the use of any as much as possible.

Also, we don’t need classes just for adding types to a few objects.

Categories
TypeScript

TypeScript any and unknown Types

In TypeScript, there’re 2 data types that can hold anything.

They’re the any and unknown types.

Since they have different names, they’re different.

In this article, we’ll look at the difference between them and what can we do with them.

The any Type

The any type variable lets us assign anything to it.

If it’s used as a parameter, then we can pass in anything.

For example, we can write:

function func(value: any) {
  const foo = 5 * value;
  const bar = value[1];
}

The TypeScript compiler doesn’t restrict what we can do with a variable or parameter that has an any type.

If we have a variable, we can assign anything to it.

For instance, we can write:

let bar: any;

bar = null;
bar = true;
bar = {};

We can assign anything to a variable with the any type.

Also, we can assign an any variable to variables, with any type:

function func(baz: any) {
  const a: null = baz;
  const b: boolean = baz;
  const c: object = baz;
}

A real example would be JSON.parse . Its signature in TypeScript’s type definition is:

JSON.parse(text: string): any;

The unknown type doesn’t exist yet in TypeScript with it’s added to the type definition, so the any type is used as the return type.

The unknown type is a better alternative to any for typing things that don’t have a known structure.

The unknown Type

The unknown type is a safer version of any .

This is because any lets us do anything, but unknown has more restrictions.

Before we can do anything with a value with an unknown type, we’ve to make the type known first by using type assertions, equality, type guards, or assertion functions.

To add type assertions, we can use the as keyword.

For example, we can write:

function func(value: unknown) {
  return (value as number).toFixed(2);
}

Since we casted the value parameter to a number with as , we can call the toFixed method with it to round the number.

The TypeScript compiler can also determine the data type with equality comparison.

For example, we can write:

function func(value: unknown) {
  if (value === 123)
    const rounded =  value.toFixed(2);
  }
}

We check if value is 123.

This way, if it is, then the TypeScript compiler knows it’s a number.

So we can call toFixed on it.

We can do the same with type guards.

To use them, we use the typeof operator to check the type.

So we can write:

function func(value: unknown) {
  if (typeof value === 'number')
    const rounded =  value.toFixed(2);
  }
}

and the compiler will also know that value is a number.

We can also use assertion functions to do the same thing.

For example, we can write:

function func(value: unknown) {
  assertNum(value);
  const rounded = value.toFixed(2);
}

function assertNum(arg: unknown): asserts arg is number {
  if (typeof arg !== 'number') {
    throw new TypeError('not a number');
  }
}

We create a assertNum function to check if arg is a number.

If it’s not, then an exception is thrown.

We then call it in our func function before doing any operation.

This way, the compiler also knows value is a number.

Conclusion

The any type is too flexible for most cases.

unknown type lets us store anything, but we’ve to determine the type of it before doing anything.