constructor signature(@TypeScript)

interfaceで定義できるconstructer signatureなるものがTypeScriptにはあります。

以下のコードでinterface SigAがそれに該当しますが、let mem = Create(ImplA)でlet mem = new ImplA()でも結果は同じですが、SigA interfaceを定義することで、コンストラクターのsignatureを分離できることになります。分離して何が嬉しいのですが、この例では嬉しさはなさそうですが色々なコンストラクタシグネチャーを定義することで定義の分離ができて見通しは良くなるように思います。

// constructor signature
type Stat = {
    [key: string]: string
}

interface A {
    get(key: string): string | null
    put(key: string, value: string):void
}

interface SigA{
    new(): A
}

function Create(constA: SigA): A{
    return new constA()
}

class ImplA implements A{
    stat: Stat = {}
    get(key: string){
        let value = this.stat[key]
        return value
    }
    put(key: string, value: string){
        this.stat[key] = value
    }
}

let mem = Create(ImplA)
mem.put('a', 'alpha')
mem.put('b', 'beta')

console.log(mem.get('a'))   // -> alpha

インスタンスを作成する関数(この場合にはcreate())が必要になるので、この部分はクラスとは別定義になります。SigAを直接的にはimplementsせずにcreate()関数で実はSigAをinplementsしていると考えても良さそうですが。

 

admin

classは名前でなく構造で型づけされる(@TypeScript)

多くの言語ではclassは名前で型づけされますが、TypeScriptのclassは構造(つまり以下の例では同じメソッドを持っていれば互換であるということ、かなり奇妙に感じますが。

// class exchangeable if it has same structure
class Cat {
    walk(){                         // only available in case of public scope
        console.log('cat walks')
    }
    cry(){
        console.log('mew')
    }
}

class Dog{
    walk(){
        console.log('dog walks')
    }
    cry(){
        console.log('bow-wow')
    }
}

function move_cry(animal: Cat){
    animal.walk()
    animal.cry()
}

let cat = new Cat()
let dog = new Dog()

move_cry(cat)
move_cry(dog)

実行結果は、

cat walks
mew
dog walks
bow-wow

move_cryの引数の型にCatを指定しているにも関わらず、Dogのインスタンスを引き渡しても問題ありません。これはCatもDogも同じメソッドを持っているからで、構造で型づけされると言われる所以です。

 

admin

abstract classとinterface

TypeScriptで改めて思うことですが、色々と世の中で議論されているのは承知のうえで、

過去の多くのオブジェクト指向言語と言われるものは、おそらくJavaが代表的ですがabstract classとinterfaceを持ちます。

どちらも例えて言えば建物ならば骨格を作る役目ですが(骨格なしでは粘土の家のように脆い作りになるから)、interfaceが全ての種類の骨格を規定するとするならばabstract classは壁とか柱の大括りのパーツを規定するようなもので、interfaceの方がより汎用性がある骨格定義方法だろうと思います。

ならば抽象化部分は全てinterfaceを使えばabstract classは不要に行き着くのですが、例えばGolangは抽象化はinterfaceだけで生態系として成り立っているわけで(Golangにはオーバライドも継承も無い、但しinterfaceで他のinterfaceは組み込めるけれども、従来型のオブジェクト指向言語の特徴は持たない)すから、おそらくinterfaceだけで不都合を感じることはないだろうと思うのでそうやってみようかと。

 

admin

VScodeでのTS(TypeScriptフォーマット)

Mac osだと、

Shift + Option + Fキー

で整形してくれました。

デフォルトでformatterがインストールされているようです。Golangだと、

% go fmt hoge.go

のようにコマンドを入れても良いですが、実は”Shift + Option + F”で同じように整形されました。ショートカットキーで扱える方が便利ですね。

 

admin

restricted polymorphism(@TypeScript)

なんのことかは、以下のコードがわかりやすいだろう、o’reillyからほぼそのままですが、


// restricted polymorphism
type TreeNode = {
    value: string
}

type LeafNode = TreeNode & {
    isLeaf: true
}

type InnerNode = TreeNode & {
    children: [TreeNode] | [TreeNode, TreeNode]
}

let a: TreeNode = {value: 'a'}
let b: LeafNode = {value: 'b', isLeaf: true}
let c: InnerNode = {value: 'c', children: [b]}

let a1 = mapNode(a, _ => _.toUpperCase())
let b1 = mapNode(b, _ => _.toUpperCase())
let c1 = mapNode(c, _ => _.toUpperCase())

function mapNode<T extends TreeNode>(       // if omit extends, node.value makes error since TypeScript
    node: T,                                // cann't define type of the value
    f: (value: string) => String
    ): T{
        return {
            ...node,
            value: f(node.value)
        }
    }

console.log(a, a1)      // { value: 'a' } { value: 'A' }
console.log(b, b1)      // { value: 'b', isLeaf: true } { value: 'B', isLeaf: true }
console.log(c, c1)      // { value: 'c', children: [ { value: 'b', isLeaf: true } ] } { value: 'C', children: [ { value: 'b', isLeaf: true } ] }

TreeNode以下をtype定義しているので、それを継承したTはnode.valueにアクセス可能、TreeNodeを継承しないでTだけならばnode.valueはTypeScriptが型を決められないのでエラーになります。

制約(この場合にはTreeNode)が複数必要な場合にはそれらを&で連結すれば良い。

 

admin

 

constとreadonly(@TypeScript)

どちらもおんなじ機能じゃない?と思いましたが当然別物です。constは変数の変更禁止(プロパティは変更できる)、readonlyはプロパティの変更禁止(変数は変更できる)の違いです。

// const vs readonly
const cons_a = {b: 2}   // variable can not be changed but property can be changed
cons_a.b = 1        // property is changeable

let const_b: {readonly b: number} = {b: 2}   // property can not be changed but variable can be changed
const_b = {b: 1}    // variable is changeable

したがってどちらも変更できないようにするには、constとreadonlyの組み合わせ使用になります。せっかくTypeScriptが型を厳密の管理、変更不可もその一部ですから、利用しない手はないでしょうから変更できない変数やプロパティは明確にしてしてコンパイラのチェック機能を使うべきです。

 

admin

簡単なgenerics(@TypeScript)

TypeScriptも静的型付け言語なのでgenericsが使えます。

以下はかんたんなサンプルコードですが、配列呼び出しにnumber[]とstring[]のケースです。

// generics
function comp<T>(arg: T[], comp: T): T[]{
    let ret: T[] = []

    for (let i = 0; i < arg.length; i++){
        if (arg[i] === comp){
        ret.push(arg[i])
        }
    }
    return ret
  }

console.log(comp([3, 2, 8, 10], 3))

console.log(comp(['3', '2', '8', '10'], '3'))

結果は想定通りに、

[ 3 ]
[ '3' ]

genericsはpolymorphism実現のための一つの手法なので、他にも実現手段はあります。

 

admin

関数の型定義と呼び出しシグネチャー(@TypeScript)

関数の型定義と呼び出しシグネチャーの例として、以下のrest parameterのコードを例にしてみます。

https://isehara-3lv.sakura.ne.jp/blog/2023/06/15/rest-parametertypescript/

// using call signature以下が呼び出しシグネチャーで書き換えてみたものですが、re_tに型情報をsrで与えているので、型のアノテーションが不要になりますから、型定義と関数の実態の分離といっても良さそうです。

// rest parameter (type safe compared to arguments method)
function sumRest(message: string, ...numbers: number[]):[string, number]{
    return [message, numbers.reduce((total, n) => total + n, 1)]
}

let res: [string, number] = sumRest('sum :', 1, 2, 3, 4, 5)
console.log(res[0], res[1] )

// using call signature
type sr = (message: string, ...numbers: number[]) => [string, number]
let re_t:sr = (message, ...numbers) => [message, numbers.reduce((total, n) => total + n, 1)]
console.log(re_t('call signature', 1, 2, 3, 4, 5))

実行結果:戻りは配列表記のままになってますが、

[ 'call signature', 16 ]

 

admin

rest parameter(@TypeScript)

関数に引数を渡す時に、不定長の場合の渡し方で配列を使った場合とrest parameterを使った場合です。

配列の場合はあくまで引数は配列が一個渡されるだけで、呼び出し側も配列で呼び出し。

rest parameterの場合には、呼び出し側は可変長引数を羅列ですから明らかに別物です。

// argument as an array
function sum(numbers: number[]): number{
    return numbers.reduce((total, n) => total + n, 1)       // the return value starts with 1
}

console.log(sum([1, 2, 3, 4, 5])) // 16

// rest parameter (type safe compared to arguments method)
function sumRest(message: string, ...numbers: number[]):[string, number]{
    return [message, numbers.reduce((total, n) => total + n, 1)]
}

let res: [string, number] = sumRest('sum :', 1, 2, 3, 4, 5)
console.log(res[0], res[1] ) // sum : 16

sumRest()の呼び出し時のresの戻り値型指定は不要ですがコメントのようなもの。sumRest()の定義側では戻り値の型指定は必須です。

 

admin

関数呼び出しのデフォルトパラメータ(@TypeScript)

関数のデフォルトパラメータを、以下でContextという形で定義した時に、関数で設定するときは以下のようなやり方。type指定で要素をオプション指定しているので、log関数では指定したい方だけ指定すれば良い。

// default parameter
type Context = {
    appID?: string,
    userID?: string
}

function log(message: string, context: Context = {userID:'aaabID'}){
    let time = new Date().toString()
    console.log(time, message, context.userID)
}

let id: Context = {userID: 'xxxyID'}
log('parameter', id)    // Wed Jun 14 2023 14:43:05 GMT+0900 (日本標準時) parameter xxxyID
log('parameter')        // Wed Jun 14 2023 14:43:05 GMT+0900 (日本標準時) parameter aaabID

TypeScriptというだけあって、型が中心にあるように感じる言語です。

 

admin