assertion(@TypeScript)

TypeScriptにおいてannotationは頻繁に登場しますが、assertionは他の言語と同じく使い方は例外的(例えばテスト用とか)だろうと思います。

以下のコードではnumber | stringの戻りの型をformatInput()の引数とするために(string型)とするためにinput as stringが使われています。”is“(user defined type guard)と似てはいますが、ここは”as“ではないといけません。

// escape hatch(assert the type)
function formatInput(input: string) {
  console.log(input)
}

function getUserInput(): number | string {
  return 123;
}
let input = getUserInput();
formatInput(input as string);
表記は、
formatInput(<string>input);
でも等価ですが、TSX(React)構文と衝突の可能性があるので”as”で明示する表現を使うべきだと。いずれにしても使用シーンは限定的で常時使う機能ではありません。

 

admin

distributive conditional type(@TypeScript)

日本語だと、分配条件型という訳になるようですが、以下のコード例でT extends U ? never : Tで表現される三項演算子的な演算で型を決めるやり方です。

TとUというジェネリック型を実際の型で呼び出すと、Uに含まれる型(boolean)はnever扱いされるようになって、number/string型だけがAの有効型となります。型の集合演算的な機能ですね。

// distributive conditional type
type WithOut<T, U> = T extends U ? never : T;
//type WithOut<T, U> = T | U
type A = WithOut<boolean | number="" | string="", boolean="">; // type of A : number/string

let a: A = "sd";
a = 123;
//a = false   // error

 

admin

user defined type guard(@TypeScript)

条件ブロック内のオブジェクトの型の制限(Type Guard)の一つのやり方としてユーザー定義(user defined)をする方法があります。typeof/instanceof/inによる方法もありますが、これらは全てインラインで行う必要がありますが、関数定義時点で型制限することで読みやすいコードになるでしょう。

<o’reillyのサンプル>

// user defined type guard
function isString(a: unknown): a is string {
  // if a is not defined, parseInput isString cause error
  return typeof a === "string";
}

function parseInput(input: string | number) {
  let formatedInput: string;
  if (isString(input)) {
    formatedInput = input.toUpperCase();
    console.log(formatedInput)
  }
}

parseInput('qwerty')

 

<TypeScript Deep Diveのサンプル>

// TypeScript deep dive sample
interface Foo {
  foo: number;
  common: string;
}

interface Bar {
  bar: number;
  common: string;
}

function isFoo(arg: any): arg is Foo {
  return arg.foo !== undefined;
}

function doStuff(arg: Foo | Bar) {
  if (isFoo(arg)) {
    console.log(arg.foo);
    //console.log(arg.bar)  // VScode detects as an error
    console.log(arg.common);
  } else {
    //console.log(arg.foo)  // error
    console.log(arg.bar);
    console.log(arg.common);
  }
}

doStuff({ foo: 234, common: "abc" });
doStuff({ bar: 567, common: "efg" });

実行結果は、

QWERTY
234
abc
567
efg

 

どちらも xx is yyyで型を限定することで意図した動作を実現できます。

 

admin

companion object pattern(@TypeScript)

サバイバルTypeScriptに書かれているように、クラスを作るまでもないけど、値(value)と型(type)を同名にして扱いを簡単にしたいというケースで使います。これはTypeScriptは値と型は異なる空間に存在しているので、同名で扱えるということに他なりません。

<Currency.ts>

type Unit = "EUR" | "GBP" | "JPY" | "USD";

export type Currency = {
  unit: Unit;
  value: number;
};

export let Currency = {
  from(value: number, unit: Unit): Currency {
    return {
      unit: unit,
      value,
    };
  },
};

 

<呼び出し側>

importは共通名Currencyで、amountDueはtypeでotherAmountDueはvalueですが、同名で扱えます。

// companion object pattern
import { Currency } from "./Currency"; // works like a class

let amountDue: Currency = {   // set object directly as a 'type'
  unit: "JPY",
  value: 12000,
};

let otherAmountDue = Currency.from(85, "EUR"); // use 'from' function as a 'value'

 

admin

Record & mapped type(@TypeScript)

https://isehara-3lv.sakura.ne.jp/blog/2023/07/02/型定義しない気持ち悪さtypescript/

の続編になりますが、recordとmapped typeというのはオブジェクトの型表現に関わるTypeScriptに固有の機能です。どちらもできることはリンク先のコードと同じですが、記述方法がよりスマートだと思います。

type WeekDay = "Mon" | "Tue" | "Wed" | "Thu" | "Fri";
type Day = WeekDay | "Sat" | "Sun";

// record type(to use above types)
let nextDay: Record<WeekDay, Day> = {
  Mon: "Tue",
  Tue: "Wed",
  Wed: "Thu",
  Thu: "Fri",
  Fri: "Sat",
};

console.log(nextDay.Wed);   //Thu

// mapped type
let nextDayM: { [k in WeekDay]: Day } = {
  Mon: "Tue",
  Tue: "Wed",
  Wed: "Thu",
  Thu: "Fri",
  Fri: "Sat",
};

console.log(nextDayM.Mon);  //Tue

この例だとrecordもmappedもできることは変わらないのですが、mapped typeの方がルックアップ型と組み合わせて使うことでrecord typeよりもできることが広いです。

また、強力なのでTypeScriptには組み込み型でmapped typeが存在していますが、Record<keys, value>などは代表的な物です。

 

admin

keyof演算子(@TypeScript)

オブジェクトのルックアップと関連しますが、オブジェクトのキーを取得できるのがkeyof演算子です。

以下のコード例ではActiveLog型のactiveLogオブジェクトを作成し、そのキーをGet型で指定してオブジェクト要素["events"] ["timestamp"]にアクセスしています。

// keyof operator
type ActiveLog = {
  lastEvent: Date;
  events: {
    id: string;
    timestamp: Date;
    type: "Read" | "Write";
  };
};

let activeLog: ActiveLog = {
  lastEvent: new Date(),
  events: {
    id: "active",
    timestamp: new Date(),
    type: "Read",
  },
};

type Get = {
  <o extends="" object="" k1="" keyof="" o="">(o: O, k1: K1): O[K1];
  <o extends="" object="" k1="" keyof="" o="" k2="">(
    o: O,
    k1: K1,
    k2: K2
  ): O[K1][K2];
};

let get: Get = (object: any, ...keys: string[]) => {
  let result = object;
  keys.forEach((k) => (result = result[k]));
  return result;
};

console.log(get(activeLog, "events", "timestamp"));

実行結果は、

2023-07-07T04:40:22.301Z

 

admin

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

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