Categories
TypeScript

Using TypeScript — Object Types and Intersections

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 by creating intersection types and checking object structures.

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.

Type Predicate Function

We can also check if a property is undefined to check if an object is of a certain type.

For instance, we can write:

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

This is similar to in except that we used typeof to check for a property instead of using the in operator.

Type Intersections

We can use type intersections to define 2 object types that are combined together.

A variable assigned to an object type should have all the properties from both types rather than some of them like union types.

For instance, we can create an intersection type and assign it to a value by writing:

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

const animal: Thing & Animal = {
  name: "james",
  breed: "dog"
};

We defined a Thing type and an Animal and combined them into an intersection type by using the & operator.

This means that animal must have properties from both Thing and Animal included.

If we skip one or more of them, then the TypeScript compiler will give us an error.

For instance, if we have:

const animal: Thing & Animal = {
  name: "jame"
};

Then we’ll get ‘Type ‘{ name: string; }’ is not assignable to type ‘Thing & Animal’. Property ‘breed’ is missing in type ‘{ name: string; }’

We can use intersection types for introducing new properties to existing objects.

For instance, if we have:

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

const thing: Thing = {
  name: "james"
};

const animal: Animal = {
  breed: "cat"
};

const cat: Thing & Animal = {
  ...thing,
  ...animal
};

Then TypeScript compiler won’t throw an error because it recognizes that name and breed are part of the Thing & Animal type.

As long as the property names and the corresponding data type match, the TypeScript compiler can figure out that it matches the structure of an intersection type.

Merging Properties with the Same Type

We can also merge types with overlapping properties.

For instance, if we have:

type Thing = { name: string };
type Animal = { name: string };

Then anything assigned to the Thing & Animal type variable would have the string property called name .

Merging Properties with Different Types

If there are properties with different types, then both properties will be merged together as an intersection.

For instance, if we have:

type Thing = { age: string };
type Animal = { age: number };

const thing: Thing = {
  age: "1"
};

const animal: Animal = {
  age: 1
};

const cat: Thing & Animal = {
  ...thing,
  ...animal
};

Then we would get an error from the compiler since age has type string & number which no value matches.

Therefore, we shouldn’t have properties with different types in the object types that we intersect.

Conclusion

We can create intersection types to merge object types into one type.

Then when a variable or parameter has the intersection type, it must include all the properties of both types, and the data type of the property must be an intersection of data types from both types.

We can check the property type with the in operator or typeof to check the structure of each object to determine their type.

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.