Categories
TypeScript

Using TypeScript — Never and Unknown Types, Removing null from Unions

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at the use of the never type, unknown type, and removing null from a union.

Never Type

TypeScript provides us with the never type for situations where the type guard has dealt with all the possible types for a value.

Once all the possible types have been handled then the compiler will only allow a value to be assigned to the never type.

For instance, if we have a switch statement:

const getTax = (price: number, format: boolean): string | number => {
  if (typeof price !== "number") {
    return 0;
  }

if (format) {
    return (price * 0.2).toFixed(2) as string;
  }
  return (price * 0.2) as number;
};

let tax = getTax(100, false);
switch (typeof tax) {
  case "number":
    console.log(`Number: ${tax.toFixed(2)}`);
    break;
  case "string":
    console.log(tax);
    break;
  default:
    let value: never = tax;
    console.log(`Unexpected type for value: ${value}`);
}

Then we assign the value for the default case to a variable with a never type since we handled the string and number in the previous cases.

Using the unknown Type

The unknown type is a safer alternative to any .

It indicates that we don’t know the type of the value.

We can assign things to an unknown type.

For instance, we can write:

let tax: unknown = getTax(100, false);

tax can’t be assigned to a variable of another type without a type assertion.

Nullable Types

null and undefined types aren’t in the TypeScript type system.

However, we can create nullable versions of variables by using nullable types.

For instance, we can write:

const getTax = (price: number, format: boolean): string | number | null => {
  if (typeof price !== "number") {
    return null;
  }

if (format) {
    return (price * 0.2).toFixed(2) as string;
  }
  return (price * 0.2) as number;
};

Now we can return null in addition to number or string .

For parameter, we can write:

const getTax = (price?: number, format: boolean): string | number | null => {
  if (typeof price !== "number") {
    return null;
  }

  if (format) {
    return (price * 0.2).toFixed(2) as string;
  }
  return (price * 0.2) as number;
};

We put a ? beside the parameter name so that we indicate that it might be null or undefined .

Restricting Nullable Assignments

We can restrict the use of null or undefined by enabling the strictNullChecks compiler setting.

If we set it to true , then we can’t assign null to something that’s not the type of variable that has other data types specified.

If we have that on, then we’ll get ‘Type ‘null’ is not assignable to type ‘string | number’.ts(2322)’.

So we’ve to add null to the type union as we have before.

Removing null from a Union

We can remove null from a union type with a non-null assertion.

For instance, if we write:

let tax: string | number = getTax(100, false)!;

Then we make sure that we don’t have null returned and assigned to tax .

We can also remove null s with type guards.

For instance, we can write:

if (tax !== null) {
  //..
}

to do null checks before proceeding.

Definite Assignment Assertion

If strictNullChecks option is enabled, the compiler will report an error if a variable is used before being assigned a value.

For instance, we can add null to the union type of the tax variable, and then assert it later.

We write:

let tax: string | number | null = getTax(100, false);

Then we can narrow it to the type we want in other statements with as or brackets.

Conclusion

The never type is used when we handled all the other types in our conditional statements.

unknown is a safer alternative to any . Variables of unknown type can’t be assigned to other variables.

There are various ways to remove a type from a union.

Categories
TypeScript

Using TypeScript — Objects

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to work with objects in TypeScript.

Shape Types

We can define shape types that restrict the types of an object.

For instance, we can write:

const obj: { foo: number; bar?: sting } = { foo: 1 };

We have an object type with the property foo that is a number and the property bar that is optional and has the data type string.

The ? after the name indicates that it’s optional.

The structure must match what’s set for the object to be assigned to the variable.

We can also include methods in object shape types.

For instance, we can write:

const obj: { name: string; greet(greeting: string): string } = {
  name: "james",
  greet(greeting) {
    return `${greeting} ${this.name}`;
  }
};

Now we have a greet method in the object shape type, which is required.

It takes a greet parameter with the data type string and also returns a string.

Like value properties, methods can also be optional.

For instance, we can write:

const obj: { name: string; greet?(greeting: string): string } = {
  name: "james"
};

Then we can omit the greet method in the object we assign to obj if we wish.

Strict Checking for Methods

We can set the strictNullChecks option to true in tsconfig.json to prevent undefined values from being set in shape types.

With it on, we can write something like:

const obj: { name: string; greet?(greeting: string): string } = {
  name: undefined
};

We’ll get the error ‘Type ‘undefined’ is not assignable to type ‘string’.ts(2322)‘.

Type Aliases for Shape Types

Since writing shape types are such a pain, we can assign it to a type alias so that we can use the alias instead of specifying the same type everywhere.

For instance, we can write:

type person = { name: string; greet?(greeting: string): string };

Now we can rewrite our assignment statement as follows:

const obj: person = {
  name: "jame"
};

Excess Properties

The TypeScript compiler is good at inferring types, which means that data types can be skipped sometimes.

For instance, if we have:

type person = { name: string };
const obj = {
  name: "jame",
  age: 10
};

const james: person = obj;

It’s smart enough to match the shape of obj and the person type and finger that obj is of type person .

Shape Type Unions

Shape types can form a union type with other shape types.

For instance, we can write:

type Person = { name: string };
type Location = { city: string };
const obj = {
  name: "jame",
  city: "new york"
};

const james: Person | Location = obj;

Person | Location is a data type that has both the properties of name and city .

Union Property Types

Properties can also have union types as their data type.

For instance, we can write:

type Person = { id: number | string; name: string };
const obj = {
  name: "jame",
  id: 1
};

Now id can be a number or a string.

Type Guards for Objects

We can add type guards for objects just like we do with other types.

For instance, we can write:

type Person = { name: string };
type Animal = { breed: string; name: string };

const person = {
  name: "jame"
};

const animal = {
  name: "jame",
  breed: "dog"
};

const arr: (Person | Animal)[] = [person, animal];
arr.forEach(a => {
  console.log(typeof a);
});

We loop through the person and animal objects which are put into the arr array.

Then we loop through them with forEach to find out the type.

Inside the callback, we use the typeof keyword to look at the type and see that they’re both type object just like they are in JavaScript.

Checking Properties

This means that we can’t use the typeof operator to check the type of objects.

Instead, we must find better alternatives.

One way is to check if a property is in the object.

For instance, we can use the in operator:

arr.forEach(a => {
  if ("breed" in a) {
    console.log("animal");
  } else {
    console.log("person");
  }
});

The in operator checks if the 'breed' property is in an object.

It checks both its own and inherited properties.

And it returns true if a property is found in any of those places and false otherwise.

However, this doesn’t help us if a property is in both types.

Conclusion

We can define object shape types.

This way, we can check the structure of the object and the property types.

We can assign them to a data type alias to make our lives easier by avoiding repetition.

Categories
TypeScript

Using TypeScript — Generic Collections and Index Types

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to extend generic collections and index types in TypeScript.

Using Generic Collections

TypeScript provides generic versions of various JavaScript collections.

They take type parameters to restrict the types of data that can be populated with them.

Map<K, V> is a JavaScript map with keys restricted to type K and values restricted to type V .

ReadonlyMap<K, V> a map that can’t be modified.

Set<T> is a set whose value type is T .

ReadonlySet<T> is a set that can’t be modified.

For instance, we can define a map by writing:

const map: Map<string, number> = new Map([["foo", 1], ["bar", 2]]);

We pass in strings and values and will get errors from the TypeScript compiler if we don’t.

Generic Iterators

TypeScript also provides us with generic versions of iterators.

Iterator<T> is an interface that describes an iterator whose next method returns IteratorResult<T> objects.

IteratorResult<T> describes a result produced by an iterator with done and value properties.

Iterable<T> defines an object that has a Symbol.iterator property and supports iteration,

IterableIterator<T> an interface that combines Iterator<T> and Iterable<T> interfaces to describe an object with the Symbol.iterator property and has the next and result property.

For instance, we can write:

function* gen() {
  yield 1;
  yield 2;
}

const iterator: Iterator<number> = gen();

We restrict the iterator to only return numbers sequentially.

Also, we can create an iterable object as follows:

const obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
  }
};

const iteratable: Iterable<number> = obj;

Then we can use it with the spread operator or the for-of loop like any other iterable object.

Creating an Iterable Class

We can create an iterable class as we did with iterable objects.

It has the Symbol.iterator method like the iterable object that we saw before.

For instance, we can write:

class GenericIterable<T> implements Iterable<T> {
  items: T[] = [];
  constructor(...items: T[]) {
    this.items = items;
  }

  *[Symbol.iterator]() {
    for (const i of this.items) {
      yield i;
    }
  }
}

We implemented the Iterable<T> interface with our own iterable class.

As long as it has the Symbo.iterator method and it’s a generator function, then it implemented the interface correctly.

Then we can use it by writing:

const itr: GenericIterable<number> = new GenericIterable<number>(1, 2, 3);

Index Types

TypeScript have index types, which we can use to restrict the values to the keys of another object.

For instance, we can write:

function getProp<T, K extends keyof T>(item: T, keyname: K) {
  console.log(item[keyname]);
}

Then K is restricted to be the key of whatever has the T type with the keyof keyword.

Then we can use it by writing:

interface Person {
  name: string;
}

const person: Person = { name: "joe" };
getProp(person, "name");

We have the Person interface with the name property, and then we can call getProp with the object created with the Person interface and the string 'name' .

If we replace the string in the 2nd argument with anything that’s not in the interface, we’ll get an error.

We can add the type parameters.

For instance, we can write:

getProp<Person, "name">(person, "name");

That works the same as we did without it.

However, we can be more restrictive with the types.

Indexed Access Operator

We can use the keyof operator in an index.

Then we can get the types of all properties and put it into one type alias as a union.

For instance, if we have the interface Person:

interface Person {
  name: string;
  age: number;
}

Then if we write:

type allTypes = Person[keyof Person];

Then allTypes is string | number .

Then we can write:

const foo: allTypes = 1;
const bar: allTypes = "foo";

As we can see, we can assign a string or a number.

Conclusion

Generic collection types are built into TypeScript.

There are types for iterables, iterators, maps, sets, and more.

There are also types for getting keys of members of other types.

Categories
TypeScript

Using TypeScript— Any and Union Types, and Type Assertions

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at the use of implicit any types, unions types, and type assertions.

Using Implicitly Defined Any Types

any types can be implicitly defined if we allow it in the compiler options.

If we set noImplicitAny to false , then we can have implicit any types.

This makes it easier to selectively TypeScript in an existing JavaScript project.

It also simplifies working with 3rd-party JavaScript packages.

For instance, we can write:

const getTax = (price) => {
  return (price * 0.2).toFixed(2);
};

Then we removed all the type annotations.

Disabling Implicit Any Types

Even though we can disable the noImplicitAny option, it’s probably not a good idea with new projects.

We set noImplicitAny to true so that it’ll check for any implicit any types in parameters and variables

For instance, if we set it to true , then we’ll know that we’re writing any code that has an implicit any type since the compiler will warn us and won’t build the code.

Using Type Unions

TypeScript allows for flexibility in our data type restrictions.

One way to make our data types more flexible is the use of union types.

We can separate each type we want to join together with | .

Union types mean that we can assign something that can be any one of the types listed.

For instance, we can write:

const getTax = (price: number, format: boolean): string | number => {
  if (format) {
    return (price * 0.2).toFixed(2);
  }
  return price * 0.2;
};

In the code above, we have getTax , which has the return string | number , which means it can return either a string or a number.

In the body, we return a string if format is true and a number otherwise.

The return type is restricted to either of those types.

Type Assertions

Type assertions tell the TypeScript compiler to treat a value as a specific type.

It’s also called type narrowing.

We can narrow a union with type assertions.

To narrow types, we can use the as operator.

For instance, given that we have the getTax function:

const getTax = (price: number, format: boolean): string | number => {
  if (format) {
    return (price * 0.2).toFixed(2);
  }
  return price * 0.2;
};

We can add type assertions by writing:

const getTax = (price: number, format: boolean): string | number => {
  if (format) {
    return (price * 0.2).toFixed(2) as string;
  }
  return (price * 0.2) as number;
};

In case people aren’t aware that toFixed returns a string, we can use as to make that clear.

Also, we can write:

const tax: string = getTax(100, true) as string;

to narrow the type of value returned by getTax to a string.

Likewise, we can do the same with number:

const tax: number = getTax(100, false) as number;

Asserting to an Unexpected Type

We can’t assert a type to something other than the expected types.

In the getTax function, we should only return a string or number.

So if we try to assert it to anything else like:

const tax: boolean = getTax(100, false) as boolean;

We’ll get an error from the TypeScript compiler.

Alternative Type Assertion Syntax

We can use brackets to add type assertions instead of the as operator.

For instance, we can write:

const tax: number = <number>getTax(100, false);

instead of :

const tax: number = getTax(100, false) as number;

They’re the same.

Type Guard

The typeof keyword can be used to test a specific type without needing type assertion.

For instance, we can write:

const getTax = (price: number, format: boolean): string | number => {
  if (typeof price !== "number") {
    return 0;
  }

  if (format) {
    return (price * 0.2).toFixed(2) as string;
  }
  return (price * 0.2) as number;
};

We have:

if (typeof price !== "number") {
  return 0;
}

to check if price is a number. If it’s not, we return 0. Otherwise, we proceed with the rest of the code.

Conclusion

We can use union types to let us assign a variable or return something with different types.

Also, we can narrow the types with the as keyword or brackets to make the type of something clear.

Categories
TypeScript

Using Static Types in TypeScript

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at the use of static types in TypeScript projects.

Static Types

TypeScript types differ from JbaScript types. We can specify many more data types than what JavaScript allows.

JavaScript is dynamically typed. This is an obstacle for programming familiar with other languages.

Values have types instead of variables in JavaScript.

For instance, if we have:

let x = 1;

Then we can get the type of the value of x which is 1, with the typeof operator.

For instance, we can write:

typeof x

and we would get 'number' .

If we don’t assign a value to something then its type would be undefined .

A value with type undefined can only be undefined .

JavaScript only has the following built-in types.

  • number — the data type for representing numeric values
  • string — the data type for representing text data
  • boolean — true or false
  • symbol —the data type for representing unique constant values
  • null — the value null , used for indicating a nonexistent or invalid reference
  • undefined — the data type used when a variable has been defined but haven’t been assigned a value
  • object — represents compound values.

Variables can be assigned anything with any of these types.

Function parameter types are also dynamic. This means that parameters can receive any kind of data.

Therefore, to make our lives easier, we can use TypeScript to help us define some types.

Creating a Static Type with a Type Annotation

We can create a static type with type annotations with TypeScript.

The most basic data types in TypeScript are static types.

We can annotate variables and parameters with data type annotations to make our assumptions explicit.

Also, we can do the same for parameters.

For instance, we can write:

const add = (a: number, b: number): number => {
  return a + b;
};

We have the add function which has the parameters a and b , both of which are numbers.

We also return a number after adding them together.

This is the most basic kind of type annotation and it’s useful to prevent making mistakes.

If we pass in anything that isn’t a number, we’ll get errors from the TypeScript compiler.

Likewise, we can add type annotations to variables.

For instance, we can write:

let price: number = 100;

to restrict price to only be assigned numbers.

Implicitly Defined Static Types

If the type is obvious from the value assigned, then we don’t have write the data type annotation explicitly.

For instance, if we have the example above:

let price: number = 100;

We can remove the type annotation and write:

let price = 100;

The TypeScript compiler will infer the type automatically.

Likewise, we can do the same for return types.

For instance, we can write:

const getTax = (price: number) => {
  return (price * 0.2).toFixed(2);
};

const halfTax = getTax(100) / 2;
console.log(tax);

We’ll get the error ‘The left-hand side of an arithmetic operation must be of type ‘any’, ‘number’, ‘bigint’ or an enum type.ts(2362)’ with the second last line.

This is because the TypeScript compiler knows that we can only divide a number by 2.

If our code has no compiler errors, then the compiler builds the code when we run it.

Photo by Mikhail Vasilyev on Unsplash

any Type

TypeScript doesn’t stop us from being flexible with data types.

It provides us with the any time to bypass the data types checks done by the compiler.

However, it does stop us from using it accidentally.

We can change getTax to:

const getTax = (price: any): any => {
  return (price * 0.2).toFixed(2);
};

so that we return anything we want and accept any kind of parameter we want.

However, we shouldn’t use any too much so that we can take advantage of the benefits of TypeScript.

Conclusion

We can use static data type for basic data type annotations.

TypeScript will also do type inference for obvious situations so we don’t have to always add data type annotations.

Also, it has the any type to let us assign anything to a variable, parameter, or return anything, but we shouldn’t use it too much.