TypeScriptにおけるstaticメンバーについて,完全に理解していますか?意味もわからず他のコードに合わせて使っている方が多いのではないでしょうか?本記事ではstaticの概要を簡単に説明し,TypeScriptとTypeScript以外の言語でのstaticの使われ方を紹介し,さらにTypeScriptを使った場合と使わない場合の比較,そしてより本質的かつ高度な応用例を紹介します.
シリーズTypeScriptで学ぶプログラミングの世界
Part1 手続型からオブジェクト指向へ
Part2 ORMってなんなんだ?SQLとオブジェクト指向のミスマッチを感じませんか?
Part3 プログラミングパラダイムの進化と革命:機械語からマルチパラダイムへ...新しいプログラミング言語に出会ってみよう!
Part4 アクセス修飾子とは? public? private? protected?
Part5 総称型(ジェネリクス・型パラメータ)とは?
他のシリーズ記事
TypeScriptを知らない方は以下の記事を参照してください
この記事はチートシートとしてシリーズ化しています.これは様々な言語,フレームワーク,ライブラリなど開発技術の使用方法,基本事項,応用事例を網羅し,手引書として記載したシリーズです.git/gh,lazygit,docker,vim,typescript,プルリクエスト/マークダウン,ステータスコード,ファイル操作,OpenAI AssistantsAPI,Ruby/Ruby on Rails のチートシートがあります.上の記事に遷移した後,各種チートシートのリンクがあります.
TypeScriptで学ぶプログラミングの世界
プログラミング言語を根本的に理解するシリーズ
情報処理技術者試験合格への道 [IP・SG・FE・AP]
情報処理技術者試験に出題されるコンピュータサイエンス用語の紹介や単語集
IAM AWS User クラウドサービスをフル活用しよう!
AWSのサービスを例にしてバックエンドとインフラ開発の手法を説明するシリーズです.
AWS UserのGCP浮気日記
GCPの様子をAWSと比較して考えてみるシリーズ
Project Gopher: Unlocking Go’s Secrets
Go言語や標準ライブラリの深掘り調査レポート
Gopher’s Journey: Exploring TCP Protocol
Goを用いてTCPプロトコルを探求 & 作成するシリーズです.
staticとは何か
「static」はクラスに紐づいたメンバーを定義する仕組みであり,インスタンス(オブジェクト)単位ではなくクラス全体で状態を共有する. 一方,staticを付けないメンバーはインスタンス単位で別々の状態を持つようになる. ここでは両者の違いに焦点を当て,実際にインスタンスを生成してみたときの結果を比較してみる.
staticを使った簡単な例
class StaticCounter {
private static count: number = 0;
constructor() {
StaticCounter.count++;
}
static showCount(): void {
console.log("Count: " + StaticCounter.count);
}
}
上記のStaticCounterクラスは,staticなint型フィールドcountと,staticメソッドshowCount()を定義している. どこからインスタンスを生成してもcountは共通の値として扱われる. 次にインスタンスを生成してみる.
class Main {
public static main(): void {
const sc1 = new StaticCounter();
const sc2 = new StaticCounter();
const sc3 = new StaticCounter();
StaticCounter.showCount(); // 結果はCount: 3
}
}
// 実行
Main.main();
上のコードを実行すると,countはクラスに対して1つだけ存在し,インスタンスを3つ生成した時点で3に到達する. また,インスタンスを生成しなくてもStaticCounter.showCount()と呼び出せるのが特徴だ.
staticを使わない簡単な例
class NormalCounter {
private count: number = 0;
constructor() {
this.count++;
}
showCount(): void {
console.log("Count: " + this.count);
}
}
NormalCounterでは,countとshowCount()にstaticを付けていないため,インスタンスごとの状態が独立する. 次にこのクラスでインスタンスを3つ生成してみる.
class Main {
public static main(): void {
const n1 = new NormalCounter();
const n2 = new NormalCounter();
const n3 = new NormalCounter();
n1.showCount(); // Count: 1
n2.showCount(); // Count: 1
n3.showCount(); // Count: 1
}
}
// 実行
Main.main();
上のコードを実行すると,インスタンスn1, n2, n3それぞれがcountを独立して持つため,いずれもCount: 1を表示する. インスタンス間で状態を共有したくない場合には,staticを使わずに設計するほうが自然になる.
staticとnon-staticの使い分け
1. 共有したいデータかどうか
全インスタンスで共有し,一意に管理すべきデータならstaticを付ける. 逆に,インスタンスごとに状態が変わるような場合にはstaticを付けないのが基本だ.
2. インスタンス生成の必要の有無
インスタンスを作らなくても呼び出したい処理や,ユーティリティ的なメソッドはstaticにすると便利だ. 逆に,インスタンスごとに異なる動作が必要ならstaticは付けない.
3. クラスデザインの複雑化
staticを使いすぎると,共有リソースが増えて状態がどこで変更されているか追いにくくなる. クラス設計を複雑にしないためにも,静的メンバーは必要最小限に抑えたほうがよい.
結論として,staticかどうかの違いは「状態をどこまで共有したいか」によって決まる. クラスレベルで管理が必要な変数やメソッドはstaticを付与し,インスタンス単位で異なる挙動が必要なら付与しないようにすることで,より適切なオブジェクト指向設計になるはずだ.
他の言語におけるstaticの例
TypeScriptはJavaScriptを拡張した言語でありながら,クラスや型注釈などの機能を提供してくれる.だがstaticという概念はTypeScript独自のものではなく,JavaやC#,C++,Pythonといった様々な言語にも存在する.ここではTypeScript以外の言語でstaticがどのように使われ,どのような利点・欠点があるのかを簡単に見てみよう.
Javaの場合は一番最後の見出しで説明しています.
C#の場合
C#でもJava同様にstaticキーワードを使って共有メンバーを定義する.クラスの初期化タイミングでstaticなフィールドが用意されるという点もJavaと似ている.特徴としては以下が挙げられる.
staticコンストラクタ (static constructor) の存在により,クラス内のstaticメンバーだけを初期化する特別なメソッドが書ける.
staticクラスという概念があり,これはクラスをすべてstaticメンバーのみで構成させ,インスタンス化を不可能にするものだ.
C#のコード例
class MyStaticClass {
public static int counter = 0;
static MyStaticClass() {
// static constructor
counter = 10;
}
public static void AddOne() {
counter++;
}
}
MyStaticClass.AddOne();
Console.WriteLine(MyStaticClass.counter);
static constructorはクラスが最初に読み込まれるタイミングで一度だけ呼ばれ,staticメンバーを初期化する役割を担う.
C++の場合
C++にもstaticは存在し,関数や変数,クラスメンバーなど様々な場面で使われる.クラススコープ(staticメンバー)だけでなく,グローバルスコープ(local static)にも使えるため,注意が必要である.またC++ではstaticメンバー変数をヘッダーファイルとソースファイルの両方に何らかの形で定義する必要があり,少々扱いに癖がある.一例を示す.
class MyStorage {
public:
static int value;
};
int MyStorage::value = 0;
int main() {
MyStorage::value = 5;
return 0;
}
このようにC++ではstaticメンバー変数を必ず一度はソースファイルで定義しなければならないという点が特徴的だ.
Pythonの場合
PythonにはJavaやC#のような明示的なstaticキーワードはないが,クラス変数と呼ばれる仕組みが存在する.クラス定義の中スクープで変数を定義すると,それは基本的に全インスタンスで共有されるものとなる.例えば下記のようになる.
class MyClass:
shared_value = 10 # これがいわゆるクラス変数
def __init__(self):
MyClass.shared_value += 1
c1 = MyClass()
c2 = MyClass()
print(MyClass.shared_value)
こうすると実行時にshared_valueがインクリメントされるたび,すべてのインスタンス間で値が共有されるため,結果としてクラスレベルで1つの値を保ったまま操作されることになる.ただし,Pythonの場合はクラスやインスタンスの属性を動的に付与・変更できる特性や,独特のスコープルールがあるため,static的な仕組みをきちんと理解するにはもう少し踏み込んだ言語仕様の理解が必要となる.
TypeScriptにおけるstatic
ここからはTypeScriptに焦点を当てる.TypeScriptはJavaScriptを基盤としており,コンパイル後はJavaScriptとして実行される.しかし,TypeScriptのコンパイラはstaticや型注釈などの構文をチェックし,JavaScriptに変換するときにそれを適切な形に落とし込む.staticメンバーの使い方は以下のようになる.
class MyClass {
static staticProperty: number = 0;
normalProperty: number = 0;
static staticMethod(): void {
console.log('static method called');
console.log(MyClass.staticProperty);
}
constructor() {
this.normalProperty = 100;
}
normalMethod(): void {
console.log('normal method called');
console.log(this.normalProperty);
}
}
TypeScriptではstaticメンバーにアクセスするとき,Javaなどと同様にクラス名から直接呼び出す.たとえばMyClass.staticMethod()のように呼び出し,そこではMyClass.staticPropertyを参照することができる.一方でnormalMethod()はインスタンス毎にuniqueなthisを受け取り,そこに格納されたnormalPropertyを参照する仕組みになっている.
staticがある場合とstaticがない場合の実用例
staticがある場合とない場合で,どのようにコードが変わり,どういった利点や欠点が生じるのかを具体的に示していく.比較の例として,簡単な「ユーザのログイン回数を記録し,その合計を参照する」機能を考えてみる.
staticを使った例
class UserLoginCounter {
static totalLogins: number = 0;
username: string;
constructor(username: string) {
this.username = username;
}
login() {
// 実際にはログイン処理が入る想定
UserLoginCounter.totalLogins++;
console.log(this.username + ' logged in');
}
static getTotalLogins(): number {
return UserLoginCounter.totalLogins;
}
}
const u1 = new UserLoginCounter('Alice');
u1.login();
const u2 = new UserLoginCounter('Bob');
u2.login();
console.log(UserLoginCounter.getTotalLogins());
この例ではlogin()
を呼ぶたびにUserLoginCounter.totalLogins
がインクリメントされるため,最終的に2になっている.staticメンバーであるtotalLoginsはクラス単位で一つだけ存在するので,複数インスタンスがあっても値は共有されている.そして合計ログイン回数がどの程度かを確認したい場合は,UserLoginCounter.getTotalLogins()
を呼ぶだけで得られる.
staticを使わない例
class UserLoginCounterNoStatic {
totalLogins: number = 0;
username: string;
constructor(username: string) {
this.username = username;
}
login() {
this.totalLogins++;
console.log(this.username + ' logged in');
}
getTotalLogins(): number {
return this.totalLogins;
}
}
const user1 = new UserLoginCounterNoStatic('Carol');
user1.login();
console.log(user1.getTotalLogins()); // => 1
const user2 = new UserLoginCounterNoStatic('Dave');
user2.login();
console.log(user2.getTotalLogins()); // => 1
この場合,totalLogins
は各インスタンスごとに保たれるため,user1
とuser2
がそれぞれ1回ずつログインしても,それらの合計値を共有することはない.もし合計ログイン数が必要なのだとすると,外部の「共通カウンタ的な変数」を処理の外側に置いて管理する必要がある.staticを使ってクラスで直接管理するか,あるいはグローバルスコープなどを使って管理するか,設計方針を検討する必要があるだろう.
staticの本質と高度な応用例
ここまでstaticの概要と,TypeScript以外の言語での例示,さらにTypeScriptでの有無比較について解説してきた.本章ではより踏み込んで,staticという仕組みの本質的な理解と応用例を紹介したい.staticが活きるのは,以下のような場面が多い.
(1) インスタンスに依存しない計算や設定情報
(2) すべてのインスタンスで一意に管理したい情報
(3) ユーティリティクラスとして機能を集約
しかし,これらの活用は時間の経過や要件の拡大に合わせて複雑化する恐れがある.staticはグローバルに近い性質を帯びるからである.プログラム規模が小さいうちは問題にならないが,大規模になったときに「なぜこの値が書き換わっているのかわからない」「テストで差し替えがうまくいかない」という問題に陥ることがある.そのため,活用にあたっては以下のような配慮や応用パターンが考えられる.
シングルトンパターンとの組み合わせ
staticはインスタンスに依存しないメンバーという性質上,シングルトンパターン (SingleTon) と非常に相性がいい.シングルトンパターンは「クラスから生成されるインスタンスを1つだけに制限する」デザインパターンだが,staticを使わずに実装することもあるものの,staticを使うことでよりシンプルに実装できる場合もある.ただし,以下のような例には注意が必要だ.
class Logger {
private static instance: Logger;
private constructor() {
// 外部からnewできないようにする
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(msg: string): void {
console.log('Log: ' + msg);
}
}
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log('Hello');
console.log(logger1 === logger2); // true
このように,instanceというstaticプロパティを持たせることでインスタンスの共通化を実現し,コンストラクタをprivateにすることで外部から複数生成されないようにしている.必要以上にシングルトンを多用するとプログラムのテスト容易性やモジュール性が損なわれるが,必要に応じてこのような設計が可能になる.
ユーティリティ関数集の作成
アプリケーションの各所で使いまわすような関数群,たとえば文字列操作や日付操作,配列操作などについてはstaticなメソッドとして雑多に集約したクラスを作ることがある.JavaScript界隈で言えばLodashやUnderscore.jsのようなライブラリがその例だが,TypeScriptでも似たようにUtilityクラスを定義できる.
class StringUtils {
static toUpperCase(str: string): string {
return str.toUpperCase();
}
static toLowerCase(str: string): string {
return str.toLowerCase();
}
// ... その他の文字列操作系メソッド ...
}
console.log(StringUtils.toUpperCase('hello'));
console.log(StringUtils.toLowerCase('WORLD'));
このようにユーティリティクラスをまとめておくと,単なる便利機能をグルーピングできる.また,コード補完にも役立つ場合がある.しかし大規模になるときは名前空間やモジュール分割の設計なども考える必要があるため,一つのクラスに大量のstatic関数を詰め込みすぎるのは避けるべきだ.
ファクトリーメソッドや抽象化パターン
オブジェクト生成を共通化するファクトリーメソッド(Factory Method)でもstaticが使われることがある.ある抽象クラスやインターフェースに対して,その実装インスタンスを生成したり,複数の派生クラスに応じた生成処理を切り替えたりするときにもstaticメソッドが便利である.
class Product {
name: string;
constructor(name: string) {
this.name = name;
}
}
class ProductFactory {
static create(type: string): Product {
if (type === 'A') {
return new Product('Type A Product');
} else if (type === 'B') {
return new Product('Type B Product');
} else {
throw new Error('Unknown product type: ' + type);
}
}
}
const p1 = ProductFactory.create('A');
const p2 = ProductFactory.create('B');
このようにProductFactoryはあくまで製品の生成だけを担う便利クラスとして設計されており,生成処理においてはstaticメソッドを呼び出すだけで済む点がシンプルだ.
乱数やID生成の管理
乱数シードや一意のID生成といった「アプリケーション全体で一貫した仕組み」を要求される箇所では,staticを使ったライブラリやユーティリティが作られる場合がある.例えば連番IDを発行するためのクラスなどを考えてみよう.
class IdGenerator {
private static currentId: number = 0;
static next(): number {
IdGenerator.currentId++;
return IdGenerator.currentId;
}
}
console.log(IdGenerator.next());
console.log(IdGenerator.next());
上記の例であれば,IdGeneratorをインスタンス化せずともnext()メソッドにより連番が追加され,アプリ全体で一意のIDが連番で取得できる設計になる.ただし,同じような仕組みが並行処理やマルチスレッドで動いたりすると,同期の問題などが起きないように十分な設計や注意が必要だ.
モジュールスコープや名前空間との比較
TypeScriptやJavaScriptでは,モジュールごとにスコープが分離される仕組みがあり,exportした変数や関数は,一度モジュールを読み込むとアプリケーション全体で同じ値を共有する. これにより,クラスのstaticほど明示的ではないものの,結果的に「静的な状態」に相当する役割を果たす場合があるのだ. 大規模なアプリケーションでクラスのstaticを乱用すると,クラスごとに多くのstaticフィールドやメソッドが散在してしまい,どこでどんな状態が共有されているのか把握しにくくなることがある. そうした場合,モジュールレベルで変数や関数を定義し,必要に応じてimport/exportする形式での設計が,管理しやすいケースは少なくない.
ここでは,カウントアップ処理を実装するときの例を挙げる. まずクラスにstaticを使う場合を見てみよう.
class StaticCounter {
static counter: number = 0;
static increment(): void {
StaticCounter.counter++;
}
static getCounter(): number {
return StaticCounter.counter;
}
}
export function useStaticCounterDemo() {
// この関数内でStaticCounterを利用する
console.log("Initial:", StaticCounter.getCounter()); // 0
StaticCounter.increment();
StaticCounter.increment();
console.log("After increment:", StaticCounter.getCounter()); // 2
}
StaticCounter
クラスは,counter
というstaticフィールドを持ち,increment()
でcounterを増やす仕組みだ. StaticCounter
をimportしたモジュールからは,インスタンスを生成しなくても直接StaticCounter.increment()
などを呼び出せる. これは手軽ではあるが,大規模になるとstaticを持ったクラスが増えてしまい,「あの共通状態はどこに書いてあったか」というのが散在しやすくなる点に注意が必要だ.
一方,同じカウントアップをモジュールスコープで表現すると,次のようになる.
let counter: number = 0;
export function incrementCounter(): void {
counter++;
}
export function getCounter(): number {
return counter;
}
export function resetCounter(): void {
counter = 0;
}
このモジュールを別の場所でimportすれば,カウンタの値は1つだけ存在し,いずれの箇所から呼び出しても同じcounterを参照する. 例えば,複数のコンポーネントに同じ値を参照させたいときに,以下のようなイメージで利用できる.
import { incrementCounter, getCounter, resetCounter } from "./counterModule";
export function useModuleScopeDemo() {
console.log("Initial:", getCounter()); // 0
incrementCounter();
incrementCounter();
console.log("After increment:", getCounter()); // 2
resetCounter();
console.log("Reset:", getCounter()); // 0
}
モジュールを読み込むときに一度初期化され,その後は同じインスタンスを共有する. クラスではないが,staticと同じく「インスタンスレスの共通状態」を提供しているわけだ. 「複数の関連する機能を1クラスに閉じ込めるか,それともモジュールレベルに分割して単機能をexportするか」は,プロジェクトの規模感や設計方針に左右されるところでもある.
TypeScriptにはnamespaceを使ってコードを論理的にまとめる方法もある. namespaceはJSにコンパイルされると単なるオブジェクトとして扱われるので,やはり「オブジェクトのプロパティとして共有状態を持つ」表現に近い. たとえば,
namespace MyLibrary {
export let libraryName: string = "DefaultLib";
export function setLibraryName(name: string): void {
libraryName = name;
}
export function getLibraryName(): string {
return libraryName;
}
}
MyLibraryは1つのオブジェクトとして解釈され,その内部のlibraryNameは静的な変数として扱われることになる. 現在ではESモジュールの利用が主流のため,namespaceを使うかどうかは好みや既存コードとの互換性などに左右されるが,コンセプトとしては「静的情報をまとめる」1つの手段だと言えるだろう.
使い分けのポイント
**1. クラス内staticのメリットとデメリット **
・メリット: 関連する処理やデータを1つのクラスにまとめられるため,OOP的な見通しが立てやすい. インスタンスを生成しなくても利用できる.
・デメリット: staticを多用すると,クラスの責務が肥大化しがちで,共有状態が散在して管理が難しくなる.
2. モジュールスコープやexportのメリットとデメリット
・メリット: 機能単位でファイルを分割できるので,関心ごとを分離しやすく,保守性を高められる. 単機能なユーティリティやデータを一括で管理する際には扱いやすい.
・デメリット: 設計を雑に行うと,どのモジュールのどの変数を使っているかが複雑化しやすい. 名前の衝突や循環参照にも注意が必要だ.
3. 名前空間の利用
・メリット: 単なるモジュールスコープに加え,複数の関連する機能をひとかたまりとしてグルーピングしやすい.
・デメリット: 多用するとJSコンパイル後に大きなオブジェクトが形成され,チームによってはESモジュールの記法と混合してしまい,可読性を落とす場合がある.
TypeScriptやJavaScriptで「インスタンスに依存しない共通状態」を定義する手法としては,クラスのstaticを使う方法だけでなく,モジュールスコープや名前空間を活用する方法もある. プロジェクトの性質や規模を考慮して,どこに共通状態を配置するかを慎重に検討することが大切だ. 1つのクラスにすべて集約するとわかりやすい反面,規模が大きくなると管理コストが跳ね上がる場合もあるので注意したい. 技術選択やチームの方針によっては,モジュールを活用して機能を小さくまとめる設計のほうが保守性や再利用性を高められる可能性が高い.
最終的には「誰がどこで状態を更新し,どこから参照されるのか」を明確に可視化できることが重要であり,そのためならstaticもモジュールスコープも適切に住み分け,あるいは共存させるのが理想的だ.
静的イニシャライザブロック (Static Initialization Blocks)
JavaScript (ES2022以降) ではクラス内でstaticブロックというイニシャライザを書くことが可能になった.TypeScriptでも対応しており,次のような書き方ができる.
class Example {
static foo: number;
static {
Example.foo = 10;
console.log('Static block called');
}
}
これにより静的プロパティの初期化や複雑なロジックをクラス読み込み時にまとめて実行できる.ただし,TypeScriptとJavaScriptの両方の最新機能であるため,ビルド設定やランタイム環境によってはサポート状況に注意が必要だ.
ジェネリクスとstaticの組み合わせ
TypeScriptではクラスにジェネリクスを指定できるが,これはインスタンスに紐づく概念である. そのためクラスレベルのstaticメンバーではジェネリクスの型パラメータを直接使用できない仕様になっている. 具体的には,以下のようにクラス宣言時に指定した型パラメータをstaticメソッドで使おうとするとエラーが起きる.
class GenericClass {
static factory(value: T): GenericClass {
// ここでコンパイルエラーとなる
return new GenericClass(value);
}
}
このように,static側はクラスに対して存在する一方,Tはインスタンスにひも付くため衝突が生じるのだ. もしstaticなメソッドでジェネリクスを扱いたい場合は,クラスの外部にジェネリック対応の関数を定義して対応する必要がある. 例えば次のように書くことが可能だ.
class GenericClass {
data: T;
constructor(data: T) {
this.data = data;
}
}
// これは外部にジェネリック関数を定義している
function createGenericInstance(value: T): GenericClass; {
return new GenericClass(value);
}
// 利用例
const numObj = createGenericInstance(123);
console.log(numObj.data); // 123
このようにクラス外のヘルパー関数を使用すれば,staticメソッドでコンパイルエラーになる問題を回避できる. また,場合によっては型パラメータを必要としないstaticメソッドにしてしまうのも手だ.
オブジェクトパターンとの比較
JavaScriptやTypeScriptでは,必ずしもクラスでstaticメンバーを多用しなくてもオブジェクトリテラルやプロトタイプベースで機能を共有するパターンがよく使われる. 例えば,以下のようにオブジェクトリテラルを使う例がある.
const Utils = {
version: "1.0.0",
doSomething(value: string) {
return value.toUpperCase();
},
settings: {
darkMode: false
}
};
export default Utils;
このオブジェクトはインスタンス化する必要がなく,モジュールとしてimportすればすぐに利用できる. つまりクラスを用いなくても,static的な機能として使いまわすことが可能なのだ.
一方で,大規模アプリケーションになると「クラスベースでstaticメンバーを活用する」「モジュール内でオブジェクトや関数だけを使う」「プロトタイプを直接操作する」などの様々な設計パターンが出てくる. クラスやstaticを乱用しすぎるとどこでどんな状態が変更されているか把握しにくくなる場合がある. また,オブジェクトリテラルやESモジュールをベースにシンプルな設計を行うことで,保守性を確保しやすいケースも多い.
従って,インスタンスにひも付らない機能を常にクラスのstaticとして格納するのではなく,モジュールスコープのオブジェクトや関数といった形で切り出すことも選択肢に入れるべきである. 特にTypeScriptではESモジュールの仕組みが標準化されているため,チームのコードベースに合わせて最適な方法を選ぶことが望ましい.
Javaにおけるstatic
JavaにおけるstaticとTypeScriptにおけるstaticは,基本的には「クラスに紐づいたメンバーを定義する」という点で似た概念だ. どちらもインスタンスごとではなくクラス単位で状態やメソッドを持つ仕組みであり,各言語のクラス設計を行ううえで欠かせない要素となっている.
ただし,Javaは静的型付け言語であり,実行時にバイトコードとして動作する. 対してTypeScriptはJavaScriptにトランスパイル可能な型付きスクリプト言語であり,実行時はJavaScriptとして動作する. そのため両者のstatic用法はよく似ているが,ランタイムや言語仕様の違いにより,細かい点では異なる挙動や注意点がある.
1. 定義と呼び出し
Javaでは,staticメンバーや静的メソッドはクラスがロードされた段階で参照可能になる. 一方TypeScriptでは,class構文として書いたstaticメンバーはJavaScriptにコンパイルされる際にクラスのプロパティを定義する記述に変換され,JavaScript実行時にクラスに付与される仕組みだ. いずれも,インスタンスを生成せずにクラス名から直接アクセスできる点は変わらない.
Javaの場合
class Sample {
static int count = 0;
static void sayHello() {
System.out.println("Hello static in Java!");
}
}
public class Main {
public static void main(String[] args) {
// インスタンスを作らなくても呼び出せる
System.out.println(Sample.count);
Sample.sayHello();
}
}
TypeScriptの場合
class Sample {
static count: number = 0;
static sayHello(): void {
console.log("Hello static in TypeScript!");
}
}
// インスタンスを作らなくても呼び出せる
console.log(Sample.count);
Sample.sayHello();
2. 継承時の挙動
JavaでもTypeScriptでもstaticメンバーはクラスに紐づくため,継承されたサブクラスからも同名でアクセスできる. ただし,サブクラスが同名のstaticメンバーを定義すると,スーパークラスのstaticメンバーを隠す(Shadowing)ような形になる. これはオーバーライドとは違い,単に同名メンバーが存在するだけなので注意が必要だ.
Javaの場合
class Parent {
static int value = 10;
}
class Child extends Parent {
static int value = 20;
}
public class Main {
public static void main(String[] args) {
System.out.println(Parent.value); // 10
System.out.println(Child.value); // 20
}
}
TypeScriptの場合
class Parent {
static value: number = 10;
}
class Child extends Parent {
static value: number = 20;
}
console.log(Parent.value); // 10
console.log(Child.value); // 20
JavaはJVM(Java Virtual Machine)上で動作し,staticメンバーへのアクセスはクラスがロードされた時点で確立している. 一方TypeScriptは最終的にJavaScriptに変換されるので,ブラウザやNode.jsなどJavaScriptの実行環境によってはモジュールのロードタイミングやスクリプトの読み込み順に左右される場合がある. ただし,TypeScriptで定義したstaticメンバーには,クラスが宣言される時点でアクセスできるという点は変わらない.
JavaとTypeScript双方に言えることだが,staticを多用してしまうとクラスに共有状態が増えすぎ,状態管理が複雑化しやすい. どこで値が変更されるか追いづらくなるため,本当にクラス全体で共有するのが望ましいデータだけをstaticにすべきだ.
まとめると,JavaのstaticとTypeScriptのstaticは「インスタンスに依存せず,クラスレベルで管理するメンバーを定義する」という意味で同じ概念だ. しかし,言語や実行環境が異なるため細かい挙動は変わる部分がある. クラスのロードや継承,モジュールの扱いなどそれぞれの言語仕様を踏まえたうえで,staticを適切に使い分けることが重要だ.