TypeScriptのクラスをnamespaceで拡張する(クラス定数、拡張メソッド、他)

  • 29
    Like
  • 0
    Comment
More than 1 year has passed since last update.

namespaceはクラスの拡張に使える

TypeScriptのnamespace1は本来の名前空間を分けるという事以外に、クラスを拡張してプロパティを生やすことができる。これを使うと本来文法的にできない、あるいは出来るけど通常の処理のコードと定義している事を明示化してメンテナンス性を向上する事が出来る。

具体的には、以下のルールに従って定義する。

  1. class→namespaceの順に同じ名前で定義する
  2. namespaceで定義するプロパティは全てexport
  3. classをexportするならnamespaceもexportする(合わせる)

ただし、 クラスではない組み込みオブジェクトを「拡張」することはできない2 (オブジェクトが上書きされてしまう)。また、 別モジュールからimportやrequireしたものも無理である (多重定義扱いでエラー)。

1. class→namespaceの順に同じ名前で定義する

class→namespaceの順に定義

class A{}
namespace A{
    export const CONST_PROP = "prop";
}
ダメな例
//namespaceを先に定義するのはダメ
namespace A{    //error!
    let a = 1;
}
class A{}

2. namespaceで定義するプロパティは全てexport

exportしないとクラスのスタティックプロパティとして見えない
class A{}
namespace A{
    let propA = 1;
    export let propB = 2;
}

A.propA;    //error!
A.propB;    //2

3. classをexportするならnamespaceもexportする(合わせる)

exportするなら両方に付けないとダメ
export class B{}    //error!
namespace B{
    export let prop = "prop";
}
class C{}           //error!
export namespace C{
    export let prop = "prop";
}

活用例

クラス定数

readonlyを待たなくてもnamespaceを使うとクラス定数が実現できる。

クラス定数風
class A{}
namespace A{
    export const CONST_PROP = "prop";
}

A.CONST_PROP;   //ok
  • メリット
    • 定数管理専用の名前を考える必要が無い(クラスと一緒に扱える)
    • TS2.0で予定のreadonlyの実装を待たなくていい
  • デメリット
    • ランタイムではただのクラスstaticプロパティ(上書き可能)
    • classと一緒に書けず、後からの定義(定数なら先に書きたい)
    • IDE上でclassとnamespace、二重の情報がでる

クラスプロパティ

クラス定数ができるなら、もちろんクラスプロパティもできる。

class A{}
namespace A{
    export let prop = "A's prop";
}

A.prop; //ok
  • メリット
    • クラスと一緒に扱える
    • 定義している箇所を明示化でき、メンテナンス性が向上
  • デメリット
    • classと一緒に書けず、後からの定義
    • IDE上でclassとnamespace、二重の情報がでる

拡張メソッド

C#にある「拡張メソッド」をTypeScriptでやるにはどうしたらいいのか?という疑問がC#から入ってきた人を中心にたまに見る。TypeScriptのnamespaceはC#の拡張メソッド相当のこともできる。


class Sample{}      //対象となるクラス

//...

namespace Sample{
    export function fn(){}
}

Sample.fn();    //ok

  • メリット
    • 定義している事がわかりやすい(Sample.fn = () => {...}とどこかの1行で定義するより明示的で、修正時にも見つけやすい)
  • デメリット
    • とはいえ、最初からstaticでクラス内で定義できるならそっちの方が分かりやすい3
    • export,functionとコード量が多くなる

豆知識編

インスタンスメソッド、インスタンスプロパティ

classinterfacenamespaceの順に定義すればインスタンスメンバー(メソッド、プロパティ)も拡張できる。

インスタンスメンバーを拡張
class A{}

//クラスと同名のインターフェイスを定義
interface A{
    //追加したいインスタンスメンバー
    fn():void;
    prop:string;
}
namespace A{
    A.prototype.fn = () => {};
    A.prototype.prop = "prop";
}

ただし、namespaceで括らなくても同じなので、明示化する以外のメリットはない。後からクラスを拡張したくなったときに、staticなメンバーと一緒に定義するのが分かりやすくなる、といったケースで役立つぐらいか。

関数やenumを拡張

実用性は低いが関数やenumも拡張できる。

関数を拡張
function test(){
    return "test";
}
namespace test{
    export let prop = "prop";
}
console.log(test()) //"test"
console.log(test.prop); //"prop"

enumの場合、number型以外のプロパティを生やせる。しかし、型チェックに使える訳ではないので実用性はかなり低い。

enumを拡張
enum Enum{
    A,
    B,
    C = 100
}
namespace Enum{
    export const STRING = "string";
    export function func(){
        return Math.PI;
    } 
}

function testEnum(arg:Enum){console.log(arg)};
testEnum(Enum.A);
testEnum(Enum.STRING);  //error
testEnum(Enum.func());  //numberを返すならok

TypeAliasを拡張

TypeAliasを拡張する事も出来る。
これとストリングリテラル型を組み合わせると、文字列Enumが同じ名前で管理できる。

type Enum = "a" | "b" | "c";
namespace Enum{
    export const A:Enum = "a"
    export const B:Enum = "b"
    export const C:Enum = "c"
}

function enumOnly(arg:Enum){}

enumOnly(Enum.A);

参考:今すぐ知るべきTypeScriptのストリングリテラル型


  1. 旧:内部モジュール 

  2. やる場合はdeclare global{interface ArrayConstructor{fn():void;}} & Array.fn = () => {...} 

  3. もっとも、拡張メソッド相当がやりたい場合はそれが難しい場合なのであまり問題ではないかも