12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptの危険性

Posted at

はじめに

筆者は現在、ESMベースの素のJavaScriptにJSDocで型情報を付与し、tsc によって型定義(.d.ts)のみを出力する構成で開発を行っています。

この構成は、TypeScriptが提供する静的解析や補完といった恩恵は享受しつつ、

  • ビルドの煩雑さ
  • 型の複雑化
  • 言語的な矛盾
  • チーム運用コストの肥大化

といったTSの構造的な問題を回避する、極めてバランスの取れた開発スタイルであると思い採用しています。

本稿では、現在もTypeScriptを信仰的に使用している開発者・チームに対し、10の視点から冷静かつ厳密に問題提起をしていきます。

1. TypeScriptは「スーパーセット」であって「上位互換」ではない

TSを盲信している方々の言動を見ていると「TypeScriptはJavaScriptのスーパーセットである」という表現が、「上位互換」「より優れている」という誤解を招いているような気がしています。

実際には、

「JavaScriptの構文や動的特性に依存しながら、それを無理に静的型で包み込むという設計」

であり、「静的型付け言語のように見えるが調和していない」ように見えます

2. 型が「あること」と「信頼できること」は別

TypeScriptには、any, unknown, as, @ts-ignore といった型システムの抜け道が大量に存在します。
言語仕様を考えると不可避な機能ですが、これらを許容しながら「型があるから安心」と語るのは、論理的に破綻してします。

静的型はあくまで補助的な安全性であり、設計の不備や思考停止を防ぐものではありません。

3. TypeScriptの型システムは「後付け」であり、整合性に欠ける

JavaやSwiftのように、クラスベースで言語の設計段階から型を中心に組み上げられた静的型言語と異なり、TypeScriptはJavaScriptに後付けで型を適用するアプローチを取っていることを忘れてはいけません。

その結果として、

  • 実行時に型が存在しない(完全に消える)
  • 構造的型(Structural Typing)による曖昧な一致
  • 関数の返り値や構造が動的に変わる設計との乖離

が発生しやすく、型が保証するはずの「正しさ」が成立しづらいです

特に、「JSライクにTSを書いている」 のであれば、すぐに ESM + JSDoc の使用を検討した方が良いと思います。
Objectに対して動的に型の違うキーを追加し、利用者がどこから引っ張れば良いか分からないような型引数を指定しないと扱えないような実装を公開することがどれだけレベルの低いことか理解しておきましょう。

AWS Amplify さん、私はあなたの defineBackend() 関数とどう向き合えば良いのでしょうか?
Gen1で簡単にできたREST APIを追加する為のGen2の酷いドキュメントが こちら です。

amplify/backend.ts
import { defineBackend } from "@aws-amplify/backend";
import { Stack } from "aws-cdk-lib";
import {
  AuthorizationType,
  CognitoUserPoolsAuthorizer,
  Cors,
  LambdaIntegration,
  RestApi,
} from "aws-cdk-lib/aws-apigateway";
import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { myApiFunction } from "./functions/api-function/resource";
import { auth } from "./auth/resource";
import { data } from "./data/resource";

const backend = defineBackend({
  auth,
  data,
  myApiFunction,
});

// ..この後クソ長いCDKスタックを記述がある

これって普通に backend を引数にした createApi() 関数を api/resource.ts に定義して使えば api の定義をまとまられます。
普通はそうしますが出来ない理由がこれです。

const backend: Backend<{
    auth: ConstructFactory<BackendAuth>;
    data: ConstructFactory<AmplifyGraphqlApi>;
    myApiFunction: ConstructFactory<ResourceProvider<...> & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>;
}>

これが backend にカーソルを合わせると出てくるヒントですが、そもそもキーが動的で、type Backend@aws-amplify/backend から取り出せますが、その型引数はどこからどう引っ張れば取り出せるのか謎。

こう言う場合、createApi() 関数はこう書くしかありません。(CDKの内容なんて Backend<any> ではとても対応できません)

amplify/api/resource.ts
import {backend} from '../backend'

export function createApi() {
  //...
}

はい、普通は引数で入れるものを export / import で直接持っていきました。

もうTS終わってませんか? 頭大丈夫ですか?

そもそも論、AWSチームの AWS SDK も Amplify もどうして常に完全リニューアルしてしまうのでしょうか?
Amplify Gen2 に至っては、完全に作りかけのモック段階なのにどうしてAWSコンソールをGen2という全く別物の仕様に作り変えてしまうのでしょう?
信頼できるベンターとして論外ですし、普通にAWSを辞めるレベルの粗悪さです。

、、、話を戻しましょう。

4. 開発者の補完体験のために、プロジェクト全体が犠牲になっている

TypeScriptの最大の魅力は、補完、ジャンプ、インテリセンスといったIDE体験の向上ですね。

しかしその代償として、

  • 複雑な型設計
  • ビルドパイプラインの増加
  • 型定義の保守コスト

が発生しており、チーム全体の生産性や開発速度を確実に損なっていると思います

テメーが気持ちよくなるために、開発コストが倍増している現実は看過できません。
そう言う場合は家で一人でデュフっていて下さい。

5. 型のための型、ユーティリティのためのユーティリティという本末転倒

TypeScriptでは、型を再利用・合成するために、さらに抽象化されたユーティリティ型が必要になる。

これにより、

  • 実装とは直接関係のないコードが大量に生まれ
  • 読みづらく、壊れやすく、テスト不能な構造

がプロジェクト内に蔓延する。

「型を守るために実装が犠牲になる」ような状態は、本末転倒以外の何物でもない

Amplifyの例もこれに当てはまります。

6. 「TSがあればスケールする」という幻想

TypeScriptの導入理由としてよく挙げられるのが、「大規模開発に耐えられる」というものです。
まるでJavaでも使っているような言い方ですね。お笑いです。

確かに、型による静的検査は一定の安心材料になるります。
しかし、実際に大規模開発において求められるのは、

  • 明確な仕様と要件の定義
  • ドメイン駆動設計(DDD)やモジュール設計の力
  • テスト戦略の充実
  • 組織的なコードレビュー体制と運用ルール

といった、言語に依存しない設計と運用の実力です。


📌 さらに深刻な問題:TypeScriptがシステムを硬直化させることすらある

TypeScriptは型の整合性を強く求めるがゆえに、ある段階を超えると変更コストが異常に高くなる傾向があります。
3で言った、TSの言語仕様と型の不整合さも相まって、

  • 型の再設計が必要になる
  • 影響範囲が不明確になり、ビルドすら通らなくなる
  • 型定義が他のモジュールに波及し、修正が伝播する

その結果、

「型があるから安心してスケールできる」はずが、いつの間にか「型のせいで一切変更が効かない」状態に陥ることがあるはずです。

ここまで来ると、チームは「一部を書き直すより、リプレイスした方が早い」という選択を迫られます。
実際に、TypeScriptによって柔軟性を失ったコードベースが、長期的な保守困難性を生み、プロダクトの寿命を縮める例もあるようです

💡 スケーラビリティの鍵は型ではなく設計

つまり、TSはスケーラビリティの「保証」ではなく「条件の一部」に過ぎません。
むしろ導入と運用を誤れば、型という名の足かせになり得ることを知ってもらいたいです。

「スケールする開発」と「TypeScriptを使うこと」はイコールではない。
真のスケーラビリティは、設計力と運用体制に宿る。

7. tscの通過が安心材料になってしまう危険性

「tscが通っているから大丈夫」という認識は、開発チーム全体の思考停止を招く最悪の兆候であると思います。

実際には、

  • テストがない
  • 実行時の型保証がない
  • バリデーションが不十分

といった問題を覆い隠し、型だけで品質が担保されたかのような幻想を生みます
特に「型でバリデーションは出来ない」と言うことは覚えておいてほしいものです。

8. 柔軟性を捨ててまで得られるものが少ない

TypeScriptを導入することで、JavaScript本来の持つ柔軟性——すぐに書けて、すぐに試せて、すぐに変えられるという特性——は大きく損なわれます。

結果として、

  • 書けるはずのコードが「型的にNG」で書けない
  • 小さな修正にも型定義の修正が必要

といった事態が発生し、設計と変更に対するストレスが不自然に高くなります

9. 現場レベルでは高コスト構造になりやすい

TSは「正しく使えば」強力なツールであることは否定しません。
しかしそれには、

  • 適切な型設計の知識
  • 組織全体での合意形成
  • 一貫した設計ルール

といった極めて高度な運用が前提となります。(それこそJavaスタイルで書くような...)
中途半端な理解と運用では、むしろ開発速度と品質の両方が損なわれる可能性が高いでしょう。

TSを採用して得られるものとは一体何なのでしょうか?

10. ESM + JSDoc + tsc はTypeScriptの理想を抽出した形である

筆者が採用している ESM + JSDoc + tsc という構成は、

  • JSの柔軟性と実行速度を損なわず
  • 補完・型チェックの恩恵だけを享受でき
  • 型定義の配布(.d.ts)も可能

という点で、静的型付けの「良い部分」だけを抽出した理想的なアプローチなように思います。

TSの導入で苦しんでいるチームがあれば、この構成は非常に有効な代替手段となるはずです


結論:TypeScriptを採用する際は慎重な判断が必要

TypeScriptは、静的型付けによる補完や型チェックの恩恵を提供する一方で、運用や設計において多くの課題を抱えています。特に、以下の点を考慮せずに導入することは、プロジェクト全体の柔軟性や生産性を損なうリスクを伴います。

  1. 型の信頼性の限界
    TypeScriptの型システムは万能ではなく、any@ts-ignore といった抜け道が存在するため、型があることが必ずしも安全性を保証するわけではありません。

  2. 運用コストの増加
    型設計や保守にかかるコストが、プロジェクトの規模やチームのスキルセットに見合わない場合、かえって開発効率を低下させる可能性があります。

  3. 柔軟性の喪失
    JavaScript本来の持つ柔軟性が制約され、小さな変更にも型定義の修正が必要になるなど、開発のスピード感が失われることがあります。

  4. 設計力の重要性
    真にスケールする開発を実現するためには、言語に依存しない設計力や運用体制が不可欠であり、TypeScriptの導入がそれを補完するものではないことを認識する必要があります。

最後に

私のチームでは絶対に採用しませんが、TypeScriptを採用するかどうかは、プロジェクトの特性やチームの状況に応じて慎重に判断しましょう。
今回の問題提起で、今一度 TypeScript について見直して頂けますと幸いです。

12
11
1

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
12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?