Skip to content

Migrate full-fledged TypeScript to TypeScript with erable type annotations only. Compatible with the type annotation proposal as well as NodeJS's strip-types

Notifications You must be signed in to change notification settings

nicojs/type-annotationify

Repository files navigation

Mutation testing badge

Type Annotationify

This is a simple tool to migrate full-fledged TypeScript code to type-annotated TypeScript code that is compatible with the type annotation proposal as well as NodeJS's--experimental-strip-types mode.

Live demo: nicojs.github.io/type-annotationify/

Example of class parameter properties transformation

Note

See running typescript natively on the NodeJS docs page for more info on --experimental-strip-types.

Status

👷‍♂️ Work in progress. This tool is still in development, and not all syntax transformations are supported yet.

Syntax Status Notes
Parameter Properties
Parameter Properties with super() call
Plain Enum
Number Enum
String Enum
Const Enum
Type assertion expressions I.e. <string>value --> value as string
Namespaces This might turn out to be impossible to do, to be investigated, see #26
Rewrite file extensions in import specifier This might be included with an option in the future

Installation

npm install -g type-annotationify@latest
# OR simply run directly with
npx type-annotationify@latest

Usage

type-annotationify <pattern-to-typescript-files>

The default pattern is **/!(*.d).?(m|c)ts?(x), excluding 'node_modules'.

This will convert all the TypeScript files that match the pattern to type-annotated TypeScript files in place. So be sure to commit your code before running this tool.

Tip

Running type-annotationify will rewrite your TypeScript files without taking your formatting into account. It is recommended to run prettier or another formatter after running type-annotationify. If you use manual formatting, it might be faster to do the work yourself

Transformations

Parameter Properties

Input:

class Foo {
  constructor(
    public bar: string,
    readonly baz: boolean,
    protected qux = 42,
  ) {}
}

Type-annotationifies as:

class Foo {
  public bar;
  readonly baz;
  protected qux;
  constructor(bar: string, baz: boolean, qux = 42) {
    this.bar = bar;
    this.baz = baz;
    this.qux = qux;
  }
}

When a super() call is present, the assignments in the constructor are moved to below the super() call (like in normal TypeScript transpilation).

The property type annotations are left out, as the TypeScript compiler infers them from the constructor assignments. This is better for code maintainability (every type is listed once instead of twice), but does come with some limitations.

Parameter property transformation limitations

  1. It assumes noImplicitAny is enabled. Without it, the inference from the assignments in the constructor doesn't work.
  2. When you use the property as an assertion function you will get an error. For example:
    interface Options {
      Foo: string;
    }
    type OptionsValidator = (o: unknown) => asserts o is Options;
    class ConfigReader {
      private readonly validator;
      constructor(validator: OptionsValidator) {
        this.validator = validator;
      }
      public doValidate(options: unknown): Options {
        this.validator(options);
        //   ^^^^^^^^^ 💥 Assertions require every name in the call target to be declared with an explicit type annotation.(2775)
        return options;
      }
    }
    The solution is to add the type annotation to the property manually.
    - private readonly validator;
    + private readonly validator: OptionsValidator;

Enum transformations

An enum transforms to 4 components. The goal is to get as close to a drop-in replacement as possible, without transforming the consuming side of enums.

Input:

enum Message {
  Start,
  Stop,
}

!NOTE String enums are also supported.

Type-annotationifies as:

type Message = 0 | 1;
type MessageKeys = 'Start' | 'Stop';
const Message = {
  0: 'Start',
  1: 'Stop',
  Start: 0,
  Stop: 1,
} satisfies Record<Message, MessageKeys> & Record<MessageKeys, Message>;
declare namespace Message {
  type Start = typeof Message.Start;
  type Stop = typeof Message.Stop;
}

That's a mouthful. Let's break down each part.

  • type Message = 0 | 1
    This allows you to use Message as a type: let message: Message. The backing value of the enum was a number (0 or 1), so thats what is used here.
  • type MessageKeys = 'Start' | 'Stop'
    This is a convenience type alias used in the object literal later.
  • The object literal
    const Message = {
      0: 'Start',
      1: 'Stop',
      Start: 0,
      Stop: 1,
    } satisfies Record<Message, MessageKeys> & Record<MessageKeys, Message>;
    This allows you to use Message as a value: let message = Message.Start. This is the actual JS footprint of the enum. The satisfies operator isn't strictly necessary, but makes sure the Message type and Message value are kept in sync if you decide to change the Message "enum" later.
  • The namespace
    declare namespace Message {
      type Start = typeof Message.Start;
      type Stop = typeof Message.Stop;
    }
    This allows you to use Message.Start as a type: let message: Message.Start.

Enum transformation limitations

  1. Type inference of enum values are more narrow after the transformation.
    const bottle = {
      message: Message.Start,
    };
    bottle.message = Message.Stop;
    //     ^^^^^^^ 💥 Type '1' is not assignable to type '0'.(2322)
    Playground link
    In this example, the type of bottle.message is inferred as 0 instead of Message. This can be solved with a type annotation.
    - const bottle = {
    + const bottle: { message: Message } = {
  2. A const enum is transformed to a regular enum. This is because the caller-side of a const enum will assume that there is an actual value after type-stripping.

FAQ

Why would I want to use this tool?

  1. You want to be alined with the upcoming type annotation proposal.
  2. You want to use NodeJS's --experimental-strip-types mode.

How does this tool work?

This tool uses the TypeScript compiler API to parse the TypeScript code and then rewrite it with type annotations.

Why do I get ExperimentalWarning errors?

This tool uses plain NodeJS as much as possible. It doesn't rely on glob or other libraries to reduce the download size and maintenance (the only dependency is TypeScript itself). That's also why the minimal version of node is set to 22.


About

Migrate full-fledged TypeScript to TypeScript with erable type annotations only. Compatible with the type annotation proposal as well as NodeJS's strip-types

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published