Introduction to type-predicates-generator, which automatically generates type guards from TypeScript type definitions
Published: 2021-11-23 05:48:54

I've created a tool to automatically generate user-defined type guards (type predicate) and assertion functions from TypeScript type definitions. By automatically generating user-defined type guards, you can protect the types of your applications safely and easily without writing wrong implementation as type predicate.

https://github.com/d-kimuson/type-predicates-generator

## type predicate and problems

If you use type annotations or `as <type>` for typing a value that comes from an external source such as API or JSON parsing, you won't be able to notice when you get an unexpected value.

```typescript
type Task = {
  id: number
  titile: string
  description: string
}

const task: Task = JSON.parse('...') // Write annotations for functions that return any type
task /* :task */ // value is typed as Task even if the type is not match to the Task type
```

If the type is different from what it actually is, it is undesirable because it reduces the benefits of TypeScript, both in terms of application safety and development experience.

In such a case, you can use [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) (`v is <type>`) to do type guarding using a user-defined function.

It allows you safe typing of untyped value.

```typescript
// define type predicate
function isTask(value: unknown): value is Task {
  return (
    typeof value === 'object' &&
    value ! == null &&
    'id' in value &&
    typeof value.id === 'number' &&
    'title' in value &&
    typeof value.title === 'string' &&
    'description' in value &&
    typeof value.description === 'string'
  )
}

// using type predicate
if (isTask(task)) {
  // if isTask returns true, assume task is of type Task
  task /* :Task */ }
}
```

This is safer than using `as` or type annotations, but TypeScript does not take care of the correct implementation of this type predicate function.

In the extreme case, a messed up implementation such as `const isTask = (v: unknown) v is Task => true` will narrow down the type to Task without getting angry.

I don't think you ever write such an implementation, of course, but

- A case where the implementation was correct at the time of writing, but the `Task` type is changed and isTask becomes an inappropriate implementation (adding a property)
- A simple implementation error (as you can see in `isTask`, it is quite complicated to write object property checks properly, and there are also shared types, array child element checks, etc., so mistakes can be made)

In some cases, incorrect runtime checking functions may be introduced due to them.

Also, since it is difficult to write type predicates for each type, psychologically speaking, it is easy to compromise with type annotations or `as` without writing the type predicate itself.

## Introduction to type-predicates-generator

The [type-predicates-generator](https://github.com/d-kimuson/type-predicates-generator) solves these problems by automatically generating a type predicate function from a type definition.

It can be installed from npm.

https://www.npmjs.com/package/type-predicates-generator

```bash
$ yarn add -D type-predicates-generator # or npm
```

The type predicate function will be generated automatically by passing the appropriate options to the `type-predicates-generator` command.

```bash
$ yarn run type-predicates-generator -f 'path/to/types/**/*.ts' -o '. /type-predicates.ts'
```

Then, automatically generates predicates functions from exported type declarations (type alias and interface) in files matching `path/to/types/**/*.ts`, and writes them to `type-predicates.ts`.

If you run type-predicates-generator on the `Task` type used in the above example, it will automatically generate the following

```ts
// auto-generated predicate definitions
import type { Task } from "path/to/your-type-declare"

const isNumber = (value: unknown): value is number => typeof value === "number"
const isString = (value: unknown): value is string => typeof value === "string"
const isObject = (value: unknown): value is Record<string, unknown> =>
  typeof value === "object" && value ! == null && !Array.isArray(value)

export const isTask = (arg_0: unknown): arg_0 is Task =>
  isObject(arg_0) &&
  "id" in arg_0 &&
  isNumber(arg_0["id"]) &&
  "title" in arg_0 &&
  isString(arg_0["title"]) &&
  "description" in arg_0 &&
  isString(arg_0["description"])
```

Now we can use the auto-generated function to protect our application by using it when values come in from the outside!

```diff
 import { isTask } from 'path/to/type-predicates'.

 fetch("path/to/api").then(async (data) => {
- const json: Task = await data.json(); // dangerous typing that doesn't check if it's really of type Task.
+ const json /* :any */ = await data.json();
+ if (!isTask(json)) throw new Error('Oops'); // throw an exception if the check fails

   json /* :Task */ }
 })
```

Now you can safely and easily type values that come from the outside.

If you specify the `-a` option, [assertion function](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion- functions) can also be generated automatically. The assertion function is more appropriate for cases like the one above.

```typescript
// which is additionally auto-generated by `-a` option
function assertIsTask(value: unkonwn): asserts value is Task {
  if (isTask(task)) throw new TypeError(`value must be Task but received ${value}`)
}

// Use
const json /* :any */ = await data.json();
assertIsTask(json) // If it fails, an exception is raised
json /* :Task */
```

Since it is auto-generated, there is no need to write a predicate, and the psychological hurdle is low.

It also supports watch mode (`-w`), so you can run watch during development and reflect changes as they happen.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2o3knfjufi3aogpxub8.gif)

See the repository for cli other options

https://github.com/d-kimuson/type-predicates-generator#cli-options

## What can be checked and what can't

Basically, I've tried to support all data structures that can be received as JSON (so if you find any missing, please raise an issue).

https://github.com/d-kimuson/type-predicates-generator/issues

On the other hand

- Functions/Methods
- Promise
- Data structures with circular references (such as `obj1.recursive = obj2, obj2.recursive = obj1`)
  - When checking properties, they go to each other to check each other's properties, resulting in an infinite loop.

It is not possible to perform checks in them.

On the other hand, all type operations are supported.

Complex types such as Mapped Types, Conditional Types, etc. can be generated without any problems, as long as they eventually resolve to JSON serializable types or their intersection or union types

```typescript
// From: Partial using Mapped Types
type PartialTask = Partial<Task>;

// Generated: generated from the last resolved type
export const isPartialTask = (arg_0: unknown): arg_0 is PartialTask =>
  isObject(arg_0) &&
  ((arg_1: unknown): boolean => isUnion([isUndefined, isNumber])(arg_1))(
    arg_0["id"]
  ) &&
  ((arg_1: unknown): boolean => isUnion([isUndefined, isString])(arg_1))(
    arg_0["title"]
  ) &&
  ((arg_1: unknown): boolean => isUnion([isUndefined, isString])(arg_1))(
    arg_0["description"]
  )
```

## Use with openapi-generator

In the front-end, some tools such as openapi-generator, aspida, etc. are used to automatically generate response types.

It is also possible to include them directly in the type-predicates-generator generation, but since the type definitions tend to be huge and it takes a long time to generate them, you can re-export only the ones you use.

(However, it took more than 5 seconds even when I tried it with a 4000-line type definition generated by [GraphQL Code Generator](https://www.graphql-code-generator.com/), which I use in my personal blog, so you may not have to worry about time to generate too much.)

For this use case, I include not only the type declarations but also the re-exported type definitions in the generation

```ts
export { Category } from ". /typescript-axios/api".
```

There is a concrete example in the repository [example](https://github.com/d-kimuson/type-predicates-generator/tree/main/example). Types re-exported by [re-export.ts](https://github.com/d-kimuson/type-predicates-generator/blob/main/example/types/re-export.ts#L4~L11) You can see that the function is generated in [type-predicate.ts](https://github.com/d-kimuson/type-predicates-generator/blob/main/example/type-predicates.ts#L100~L124) from the type definition re-exported by [re-export.ts](https://github.com/d-kimuson/type-predicates-generator/blob/main/example/type-predicates.ts#L100~L11). From the type definition re-exported by [type-predicate.ts](~L11), we can see that the function has been generated

## How is it implemented?

The Compiler API extracts the type information from the Glob-specified file and generates it automatically.

I'll leave the details of the Compiler API to another article.

1. search [Node](https://typescript-jp.gitbook.io/deep-dive/overview/ast#node) for the matched file and pick up the declaration node of type alias and interface. 2.
2. recursively pick up the types from the declaration nodes using [TypeChecker](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker), and 3. writes the exported type information as [Exportable Type Information](https://github.com/d-kimuson/type-predicates-generator/blob/main/src/type-object.ts), which is defined independently.
3. generate code based on the exported type information

This is how it works

https://github.com/d-kimuson/type-predicates-generator/blob/main/src/compiler-api/compiler-api-handler.ts

https://github.com/d-kimuson/type-predicates-generator/blob/main/src/generate/generate-type-predicates.ts

## Other solutions

There is a major library [io-ts](https://www.npmjs.com/package/type-predicates-generator) that solves the problem that type predicate implementation may deviate from the type definition.

```ts
// Usage Example
import { isRight } from "fp-ts/lib/Either";
import * as t from 'io-ts'.

const TaskIO = t.type({
  id: t.number,
  title: t.string,
  description: t.string,
})

export type Task = t.TypeOf<typeof TaskIO>

// Example usage
const data /* :any */ = JSON.parse('...')
const result = TaskIO.decode(data)
if (isRight(result)) {
  const task /* :Task */ = result.right
}
```

io-ts uses its own notation to define the types to be checked at runtime, and accepts TypeScript types from there (`t.TypeOf(typeof Task)`).

It's a very nice library, but the type definitions can't be written in TypeScript types.

- Define an API type from an existing TS type through type operations.
- Define API types from existing TS types through type operations.

In such a case, type-predicates-generator is a good choice.

## Conclusion

So, that's my introduction to type-predicates-generator!
It's still a work in progress, but it's an easy tool for improving type safety, so I hope you'll give it a try!
PR and issues are welcome!