iTranslated by AI
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.
Discussion