iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
😸

Changing Runtime Behavior Using TypeScript Types

に公開

TypeScript is a language based on the premise of transcompilation. The results of transcompilation do not correspond one-to-one with JavaScript, and it is not essentially JavaScript with types attached. It is necessary to clearly recognize that the results of transcompilation affect behavior at runtime.

Using Type Information at Runtime

Prerequisites

Add the following settings to tsconfig.json. No special compiler plugins are required.

{
  "compilerOptions": {
   "experimentalDecorators": true
   "emitDecoratorMetadata": true
  }
}

Sample Code

import "reflect-metadata";

// Type check
function isType(type: object, value: unknown) {
  switch (type) {
    case Number:
      if (typeof value !== "number") return false;
      break;
    case String:
      if (typeof value !== "string") return false;
      break;
    case Boolean:
      if (typeof value !== "boolean") return false;
      break;
    case Array:
      if (!(value instanceof Array)) return false;
      break;
    case Function:
      if (!(value instanceof Function)) return false;
      break;
  }
  return true;
}

function CHECK(target: any, name: string, descriptor: PropertyDescriptor) {
  const ptypes = Reflect.getMetadata(
    "design:paramtypes",
    target,
    name
  ) as object[];
  const rtype = Reflect.getMetadata(
    "design:returntype",
    target,
    name
  ) as object[];
  return {
    ...descriptor,
    value: function (...params: unknown[]) {
      if (ptypes.length !== params.length) throw "引数の数が不正";
      const flag = ptypes.reduce((a, b, index) => {
        return a && isType(b, params[index]);
      }, true);
      if (!flag) {
        throw "引数の型が不正";
      }
      const result = descriptor.value.apply(this, params);
      if (!isType(rtype, result)) throw "戻り値の型が不正";
      return result;
    },
  };
}

// Test class (no type check)
class NoCheck {
  func01(a: number, b: string, c: boolean): number {
    console.log(a, b, c);
    return 0;
  }
  func02(a: number, b: string, c: boolean): string {
    console.log(a, b, c);
    return 0 as never; // Invalid return type
  }
}

// Test class (with type check)
class Check {
  @CHECK // When added, parameter and return types are checked at runtime
  func01(a: number, b: string, c: boolean): number {
    console.log(a, b, c);
    return 0;
  }
  @CHECK
  func02(a: number, b: string, c: boolean): string {
    console.log(a, b, c);
    return 0 as never; // Invalid return type
  }
}

console.log("--- No Check ---");

// Create instance
const noCheck = new NoCheck();

// Normal execution
noCheck.func01(0, "A", true); // OK

// Pass wrong parameter types
try {
  noCheck.func01(0, 10 as never, true);
} catch (e) {
  console.error(e);
}

// Call a method with an incorrect return value
try {
  noCheck.func02(0, "A", true);
} catch (e) {
  console.error(e);
}

console.log("--- Check ---");

// Create instance
const check = new Check();
// Normal execution
check.func01(0, "A", true); // OK

// Pass wrong parameter types
try {
  check.func01(0, 10 as never, true); // Exception: "引数の型が不正"
} catch (e) {
  console.error(e);
}

// Call a method with an incorrect return value
try {
  check.func02(0, "A", true); // Exception: "戻り値の型が不正"
} catch (e) {
  console.error(e);
}

Execution Results

In the Check class, the types of parameters and return values are checked at runtime by referring to TypeScript's type information.

--- No Check ---
0 A true
0 10 true
0 A true
--- Check ---
0 A true
引数の型が不正
0 A true
戻り値の型が不正

In this instance, I used TypeScript Decorators to check the types of parameters and return values at runtime. Well-known packages that utilize such Decorators include NestJS and TypeORM.

Differences in Behavior Due to Configuration Changes

This is one of the important points to note. Since the JavaScript output varies depending on the settings, it's necessary to fully realize that TypeScript is a multi-functional transcompiler.

  • Source
export class Test {
  a: number | undefined;
}

const test = new Test();
console.log(Object.keys(test));
  • ES2015
{
  "compilerOptions": {
    "target": "ES2015"
  }
}
[]
  • ESNext
{
  "compilerOptions": {
    "target": "ESNext"
  }
}
[ 'a' ]

Summary

TypeScript is often perceived as just adding types to JavaScript, but in reality, it is a multi-functional transcompiler. The code generated after compilation is not simply the original code with types removed. Especially when creating packages that support both ESM and CJS, the code for importing/exporting modules can differ greatly depending on the settings, and I think many people struggle with this. It's important to keep the behavior of the output code in mind while using it.

GitHubで編集を提案

Discussion