pbialy
pbialy

Reputation: 1083

Typescript - check type in runtime and warn about errors

INTRODUCTION

I'm using Angular 6.

Angular uses typescript, so you can type your function's arguments:

public fun1(aaa: number) {
  console.log(aaa);
}

If I'll try to call fun1 with parameter of other type - I'll get an error:

public fun2() {
  this.fun1([1, 2, 3]); // TS2345: Argument of type 'number[]' is not assignable to parameter of type 'number'
}

THE PROBLEM

Type checking works well if I can control the arguments in my files.

But maybe fun1 is called with some parameters which I get from backend.

Typescript doesn't workin in runtime, so it won't show any errors, the code of fun1 would just run with [1, 2, 3].

-- edited --

Backend response is not the only source of problem. Sometimes a library can change type of your data and you might not know about it. An example would be using Reactive Forms with a control which should be a number - it's converted to string when you edit the number.

QUESTION

Is there some common way to handle type checking in runtime?

I thought about something like:

public fun1(aaa: number) {
  if (typeof aaa !== 'number') {
    console.warn('wrong type');
  } else {
    console.log(aaa);
  }
}

, but WebStorm then tells me typeof check is always false: 'aaa' always has type 'number'.

Also putting something like this at the top of every function seems like adding a lot of unnecessary code.

Does someone have a good solution for this?

Upvotes: 5

Views: 4578

Answers (1)

catchergeese
catchergeese

Reputation: 732

TypeScript - by design - does not provide runtime type information (see https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals, Non-goals, point 5).

If you feed TypeScript with some external data (maybe passed in data-attributes in HTML or fetched from REST API), TypeScript cannot know about its type. But there are some solutions. We'll have a look at two of them.

One approach you can take is to circumvent type system with as operator. This is not a type safe approach (you introduce a hole in a type system) but it's (at least partially) in line with TypeScript goals (see a link above).

Let's pretend that fetchApiSync function call returns just a simple javascript object {name: "John", age: 42} and we have a function:

function introduceMe(person: Person) {
    return `My name is ${person.name} and I'm ${person.age} years old.`
}

along with an interface

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

Now let's do the following:

const person: Person = fetchPersonFromApi(1);.

Clearly, we would like fetchPersonFromApi to return a value that matches Person interface so that we can do:

const introduction = introduceMe(person);

To achieve it, we can do something like this:

function fetchPersonFromApi(id: number): Person {
   // it's synchronous and without error handling just for brevity
   const response: Person = fetchApiSync(`/persons/${id}`) as Person

   return response;
}

It type checks as we did an explicit cast of whatever we get from an API to a Person when we used as Person. It's "necessary" because fetchApiSync has no way to know about the types of the values that it gets from an external API. Of course, we run into problems when instead of a value of type Person we get a value of some other type but there is nothing that we can do about it... or can we?

Another variant of this option is to use any (or unknown from TS 3) types. These two allow you to skip the type checks - value of type any can be assigned to "any" type, including Person. But errors can be hard to trace as you have an unchecked value inside your type system. Use it wisely!

Second approach that you can take is to use io-ts library (https://github.com/gcanti/io-ts). It allows you to specify runtime types and perform type checks on runtime (hence, it introduces an overhead of validating if a value matches a type). Moreover, you need to specify those types in slightly different manner (see documentation) so that they can exist on runtime. Good thing to note is that io-ts allows you to derive your pure-TypeScript type from io-ts type definitions. Using io-ts allows you to stay more type safe and protect your codebase from nasty casting or using any type (or unknown type from TypeScript 3). It does so at some cost - a part of which is non-negligible complexity.

Upvotes: 5

Related Questions