2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript、ついに「型」が実行時に書けるようになりました — setterで型安全を実現する新提案「Types as Properties」—

2
Last updated at Posted at 2026-03-31

はじめに

※この記事は、巻末のコードをAIに解説してもらったものです。

JavaScriptに型が欲しい、と思ったことはありませんか?

でもTypeScriptはビルドが必要だし、
ランタイムで保証されるわけでもない。
最近話題の「Type Stripping」も、結局は型を消して実行しているに過ぎません。

そこで今回紹介するのが、実行時に型を強制する新しい書き方です。

それが――

Types as Properties

です。


型は「値として代入できる」

まずはこれを見てください。

const locals = {
  num: Type.any = 1,
  str: Type.any = "Hi",
  flg: Type.any = true
};

一見ただの代入ですが、ここで重要なのは

Type.any = 値 という構文そのものが「型付け」になっている

という点です。

しかも any なので何でも通ります。


setterで型チェック

次に number 型を定義します。

Object.defineProperty(Type, "number", {
  set: value => {
    if(typeof value !== "number")
      throw new TypeError(`${typeof value} is not number`);
  }
});

これだけで…

this.number = 1;     // OK
this.number = "hi";  // ❌ error

ランタイム型チェックが完成しました。


配列も型付けできる

this.numbers = [1, 2, 3]; // OK
this.numbers = [1, null]; // ❌ error

さらに…

[
  this.number,
  this.number,
  this.number
] = [4, 5, 6];

分割代入すら型チェック対象になります。


オブジェクトも安全に

this.accessor({
  x: this.number = 0,
  y: this.number = 0
});

これで

point.x = "hello"; // ❌

のような不正代入も防げます。


関数型まで書ける

Type.def({
  Add: number => number => number
});

使い方:

const add = Type.Add((x, y) => x + y);

add(1, 2);     // 3
add(1, "hi");  // ❌

ついにJavaScriptで

関数シグネチャが実行時に保証される時代へ


まとめ

この仕組みを使えば:

  • 型は「宣言」ではなく「代入」で書ける
  • setterで実行時検証
  • 分割代入にも対応
  • 関数型もサポート

つまり…

JavaScriptそのものが型システムになる


※追記(重要)

この記事はエイプリルフールネタです。

ただしコードは実際に動きます。


懸念点(現実)

冷静に見るといくつか問題があります:

1. 可読性がかなり低い

x: this.number = 1

は普通のJS開発者にはほぼ読めません。


2. 副作用が強すぎる

Type.number = value

が「検証だけして値を保持しない」というのは直感に反します。


3. toString()依存の関数型パース

number => number => number

を文字列分解しているため、

  • minifyで壊れる
  • 書き方に依存する

という問題があります。


4. 実行コスト

すべての代入でチェックが走るため、
パフォーマンスはそれなりに犠牲になります。


5. TypeScriptの完全下位互換ではない

  • 静的解析できない
  • IDE補完が効かない

それでも面白い理由

このコードの本質は:

型を「メタ情報」ではなく「振る舞い」として扱っている

点です。

これはかなり本質的で、

  • Proxy
  • バリデーションライブラリ
  • ランタイム型チェック

などに繋がる発想です。


結論

ネタとしてはこう言えます:

JavaScriptに型は不要です。
なぜなら、もう書けるからです。

(ただし正気は失います)

おまけ(全コード)

class Type {
  static get any() {
    return void 0;
  }

  static set any(value) {}
}

class AnyExample {
  static main() {
    try {
      const locals = {
        num: Type.any = 1,
        str: Type.any = "Hi",
        flg: Type.any = true
      };

      console.table(locals);

      locals.num = "no error";
      console.log(locals.num);
    } catch(e) {
      console.error(e.message);
    }
  }
}

AnyExample.main();

Object.defineProperty(Type, "number", {
  get: () => 0,
  set: value => {
    if(typeof value !== "number")
      throw new TypeError(`${typeof value} is not number`);
  },
  configurable: true,
  enumerable: true
});

class NumberExample extends Type {
  static main() {
    try {
      const locals = {
        zero: this.number,
        one:  this.number = 1,
        two:  this.number = 2
      };

      console.table(locals);

      this.number = "string is not number"; // error
    } catch(e) {
      console.error(e.message);
    }
  }
}

NumberExample.main();

Object.defineProperties(Type, {
  Array: {
    get: () => [],
    set: values => {
      if(!Array.isArray(values))
        throw new TypeError(`${typeof values} is not Array`);
    },
    configurable: true,
    enumerable: true
  },

  numbers: {
    get: () => [0],
    set: values => {
      values.forEach((value, i) => {
        if(typeof value !== "number")
          throw new TypeError(`[${i}]: ${typeof value} is not number`);
      });
    },
    configurable: true,
    enumerable: true
  }
});

class ArrayExample extends Type {
  static main() {
    try {
      const locals = {
        basicArray: this.Array = [0, "", false],
        typedNumberArray: this.numbers = [1, 2, 3],

        destructuredTuple: [
          this.number,
          this.number,
          this.number
        ] = [4, 5, 6],

        objectDestructuredTuple: {
          0: this.number,
          1: this.number,
          2: this.number
        } = [7, 8, 9],

        hybridArray: [
          this.any,
          ...this.numbers
        ] = [null, 10, 11]
      };

      console.table(locals);

      //this.Array = "string is not Array"; // error
      this.numbers = [null]; // [0]: object is not number
    } catch(e) {
      console.error(e.message);
    }
  }
}

ArrayExample.main();

Type.of = value =>
  value === null ? "null"
    : typeof value !== "object" ? typeof value
    : value.constructor && value.constructor.name ||
    //: value?.constructor?.name ||
      Object.prototype.toString.call(value).slice(8, -1);

class Property extends Type {
  constructor() {
    super();
  }

  accessor(props) {
    for(const [name, initValue] of Object.entries(props)) {
      let descriptor = null;
      let validate = null;
      let _value = initValue;
      const typeName = Type.of(initValue);
      const self = this;

      if(Type.of(initValue.get) === "function" ||
         Type.of(initValue.set) === "function") {
        descriptor = {
          configurable: true,
          enumerable: true,
          ...initValue
        };

      } else {
        if(initValue === null)
          validate = value => value === null;

        else if(typeof initValue !== "object")
          validate = value => Type.of(value) === typeName;

        else
          validate = value => value instanceof self[typeName];

        descriptor = {
          get: () => _value,
          set: value => {
            if(!validate(value))
              throw new TypeError(`${Type.of(value)} is not ${typeName}`);
            _value = value;
          },
          configurable: true,
          enumerable: true
        };
      }
      Object.defineProperty(this, name, descriptor);
    }
  }

  readonly(props) {
    for(const [name, _value] of Object.entries(props)) {
      let descriptor = null;

      if(Type.of(_value.get) === "function")
        descriptor = {
          configurable: true,
          enumerable: true,
          ..._value
        };

      else
        descriptor = {
          get: () => _value,
          set: value => {
            throw TypeError(`constants cannot be changed`);
          },
          configurable: true,
          enumerable: true
        };
        
      Object.defineProperty(this, name, descriptor);
    }
  }
}

Type.def = function(defs) {
  for(const [name, value] of Object.entries(defs)) {
    let descriptor = null;

    if(Type.of(value) === "function") {
      let paramTypes = value.toString().split(" => ");
      let returnType = paramTypes.pop();
      const self = this;

      descriptor = {
        get: () => function(proc) {
          return function(...params) {
            if(params.length !== paramTypes.length)
              throw Error(`Parameter mismatch: expected ${paramTypes.length} got ${params.length}`);

            params.forEach((param, i) => {
              self[paramTypes[i]] = param;
            });

            const result = proc.call(this, ...params);

            self[returnType] = result;
            return result;
          };
        },
        configurable: true,
        enumerable: true
      };
    } else {
      descriptor = value;
    }

    Property.prototype.readonly.call(this, {[name]: descriptor});
  }
};

Object.defineProperty(Type.prototype, "number", Object.getOwnPropertyDescriptor(Type, "number"));

class Point extends Property {
  constructor(x = 0, y = 0) {
    super();

    this.accessor({x: this.number = x});
    this.accessor({y: this.number = y});
  }
}

Type.def({
  Point: {
    get: () => Point,
    set: value => {
      if(!(value instanceof Point))
        throw new TypeError(`${Type.of(value)} is not Point`);
    }
  }
});

class PointExample extends Type {
  static main() {
    try {
      const locals = new Property();

      locals.accessor({
        defaultPoint: this.Point = new Point(),
        
        initializedPoint: {
          x: this.number,
          y: this.number
        } = new Point(1, 1)
      });

      console.table(locals);

      locals.defaultPoint.x = 1;
      console.table(locals.defaultPoint);

      //this.Point = null; // null is not Point
      locals.defaultPoint.y = false; // boolean is not number
    } catch(e) {
      console.error(e.message);
    }
  }
}

PointExample.main();

class TypedefExample extends Type {
  static main() {
    try {
      this.def({
        Add: number => number => number,
        One: this.number = 1,
        Two: this.number = 2
      });

      const add = this.Add((x, y) => x + y);

      console.log(add(this.One, this.Two)); // 3
      console.log(add(this.One, "string is not number")); // error
    } catch(e) {
      console.error(e.message);
    }
  }
}

TypedefExample.main();

tio.run

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?