Open18

個人的に忘れがちなTSの基礎文法に関わるなぜのメモ

Kumamoto-HamachiKumamoto-Hamachi

インデックス・シグネチャ=>代替策としてのMap

(TODO:もう少し分かりやすく正確に書く)

値の型はそろえないとダメ。逆にるインデックスシグネチャを複数使うならインデックスの型は揃えちゃだめ!なぜ?

// OK
type OkCompany = {
      name: string
     [key: string]: string;
     [key2: number]: string;   
}
// OK
type OkCompany2 = {
     [key: string]: boolean;
     [key2: number]: boolean;

// NG
type NgCompany = {
     [key: string]: string;
     [key2: number]: number;

It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, JavaScript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same thing as indexing with "100" (a string), so the two need to be consistent.

TypeScript: Documentation - Object Types

keyがnumber型であってもJSだとそれがconvertされてstringになってしまう。なので100とkey名にしていてもそれが"100"にconvertされちゃう。それだと結局値の型が違うとおかしなことに。

インデックス・シグネチャと使うと型安全ががが...

  • (インデックス・シグネチャが)ない時・ある時比較
    存在しないキーにアクセスしてみる
// ない時
const user: User = {
    name: "Kumamoto",
    age: 28
}
// error TS2339: Property 'hoge' does not exist on type 'User'.
console.log(user.hoge)

// ある時
const user2: User2 = {
    name: "Kumamoto",
}
// これはundefinedになる(危険!)
console.log(user2.hoge)

TODO
代替策:Map

TypeScript の「型安全」を担保するために知っておきたかったこと - OITA: Oika's Information Technological Activities

Kumamoto-HamachiKumamoto-Hamachi

contextual typing(文脈に基づく型付け)

  • 普通の推論:「代入する式」に付与される引数や返り値の型から「代入される変数」の型を推論
  • contextual typing:「代入される変数」の型注釈から「代入する式」の型を推論

TODO

TypeScript: Documentation - Type Inference

Kumamoto-HamachiKumamoto-Hamachi

分割代入(配列・オブジェクト)

配列での分割代入

const animals = ["kuma", "neko", "inu"]
const [bear, cat, dog] = animals

配列の中のオブジェクト

TODO

オブジェクトでの分割代入

const apple = {
    color: 'red',
    price: 300,
}
const { color, price } = apple

オブジェクトでの分割代入(ネスト)

const company = {
    id: 2000,
    corporate_info: {
        corp_name: 'Kuma産業',
        industry: '医療',
    }
}

const { id, corporate_info: { corp_name } } = company

デフォルト値

const apple = {
    color: 'red',
    price: 300,
}
const {size:number = 10} = apple

restパターン

TODO

スプレッド構文(spread syntax)

オブジェクト作成時に別のオブジェクトからコピーとかするのに使う

const obj1 = {
    a: 1,
    b: 2,
}
const obj2 = {
  c: 3,
  ...obj1
}

Kumamoto-HamachiKumamoto-Hamachi

コールシグネチャ

TODO
プロパティの定義
TypeScript: Documentation - More on Functions

複数のコールシグネチャを持たせて関数オーバーローディングっぽいのを楽に実現

type SwapFucn = {
  (arg: string): number;
  (arg: number): string;
}

※参考:プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで (Software Design plus) | 鈴木 僚太 4.2.5 コールシグネチャによる関数型の表現

Kumamoto-HamachiKumamoto-Hamachi

部分型関係(特に引数の話)

反変

TODO
引数に突っ込むと部分型の関係が逆に!(contravariant)

type Person = {
    name: string;
    age: number;
}

type Engineer = {
    name: string;
    age: number;
    company: string;
    favorite_language: string;
}

const person: Person = {
    name: 'John',
    age: 27,
}
const kumamoto: Engineer = {
    name: 'John',
    age: 27,
    company: 'Apple',
    favorite_language: 'Ruby',
}
const receive_person = (person:Person) => 'person'
const receive_eng = (eng: Engineer) => 'engineer'
// これはOK
receive_person(person)
receive_person(kumamoto)
receive_eng(kumamoto)
// これはNG
// receive_eng(person)


// よりわかりやすくすると...
type ReceivePerson = (person: Person) => string
type ReceiveEngineer = (person: Engineer) => string
const receive_person:ReceivePerson = (person:Person) => 'person'
const receive_eng: ReceiveEngineer = (eng: Engineer) => 'engineer'

// これはOK
const p1: ReceivePerson =  receive_person
const e1: ReceiveEngineer =  receive_person
const e2: ReceiveEngineer =  receive_eng

// これはNG
const p2: ReceivePerson =  receive_eng


Covariance and Contravariance in TypeScript

TypeScript: Documentation - TypeScript 2.6

引数の数の違い

TODO

Kumamoto-HamachiKumamoto-Hamachi

部分型(subtyping relation)の基本

構造的部分型(structural subtyping)
※他のObjective Langならnominal subtyping(名前的部分型)が多い

type Person = {
    name: string;
    age: number;
}

type Engineer = {
    name: string;
    age: number;
    company: string;
    favorite_language: string;
}

// これはOK
const kumamoto: Engineer = {
    name: 'John',
    age: 27,
    company: 'Apple',
    favorite_language: 'Ruby',
}
const person: Person = kumamoto;

// これはNG
const person: Person = {
    name: 'John',
    age: 27,
    company: 'Apple',
    favorite_language: 'Ruby',
}

下のNGは

index.ts(20,7): error TS2451: Cannot redeclare block-scoped variable 'person'.
index.ts(23,7): error TS2451: Cannot redeclare block-scoped variable 'person'.
index.ts(26,5): error TS2322: Type '{ name: string; age: number; company: string; favorite_language: string; }' is not assignable to type 'Person'.
  Object literal may only specify known properties, and 'company' does not exist in type 'Person'.

Process finished with exit code 2

companyという余計なプロパティが指摘されているがこれはなぜだろうか?だって部分型でOKになるはずじゃ....と最初思う。

が、これはObject literal mayとあるようにオブジェクトリテラルに関してのみこれは出る。OKの時と違いオブジェクトリテラルを突っ込むのは「流石に無駄でしょ?typoでしょ?」と気を利かせてくれているようだ。

What do we do about object literals that make use of structural subtyping in 1.6? · Issue #4234 · microsoft/TypeScript

2つの「ない」

「型にはない」が「実際にはある」...みたいなことがある

TSは型上にないプロパティへのアクセスを許可しない。
しかし「型にないプロパティ」は「実際にあるかどうかわからない」というこちしかわからない。

type Person = {
    name: string;
}
type AdultPerson = {
    name: string;
    age:? number;
}
const person = {
    name: 'kuma',
    age: 27
}
const person2 = {
    name: 'siro',
}

// error TS2339: Property 'age' does not exist on type 'Person'.
const kuma: Person = person;
console.log(kuma.age)

// ok [2つ目はundefinedに、型としてはnumber | undefinedの合併型(ユニオン型)になる]
const smith: AdultPerson = person;
console.log(smith.age)
const bob: AdultPerson = person2;
console.log(bob.age)
Kumamoto-HamachiKumamoto-Hamachi

部分型 その他

関数型のメソッド記法

TODO:なぜゆるくなる?

https://twitter.com/digitalhimiko/status/1580963208458039297?s=20&t=n5NBr3xmDqIbAdPpIFcu_g

読み取り専用

普通の配列は読み取り専用配列の部分型
オブジェクト型だとTSの不完全な部分が...

部分型の気持ち(自己理解)

要は上位互換。AがBの部分型であるとき

受け取る側がAを期待しているのにBを出したら「これ下位互換じゃねーか💢」となるし、受け取る側がBを期待していてAを出したら「おまけしてくれてありがと(使わんからどうでも良い情報あるけど)」となる。

引数の反変の話もこれで考えると...(TODO)

Kumamoto-HamachiKumamoto-Hamachi

ジェネリクス

『型引数を受け取る関数を作る機能』のこと(BY プロを目指すためのTYPESCRIPT入門)
型引数を持つ型をジェネリクス型と呼ぶ。

type Kumamoto<T> = {
   name: string;
   job:T;
}

TypeScript: Documentation - Generics
ジェネリクス(ジェネリックプログラミング)とは - 意味をわかりやすく - IT用語辞典 e-Words
【TypeScript】Generics(ジェネリックス)を理解する - Qiita

配列とジェネリクス

T[]Array<T>の省略。

Kumamoto-HamachiKumamoto-Hamachi

雑多な話

取りうる値

値をデータの源としたい時はas constを使おう

// readonly ['slow', 'medium', 'fast']
const speedList =  ['slow', 'medium', 'fast'] as const;
// 'slow' | 'medium' | 'fast'
type Speed = (typeof speedList)[number];


type Speed = 'slow' | 'medium' | 'fast'

参考:TypeScriptの型演習 - Qiita

例外処理と型

「try-catch」よりも「失敗を表す返り値」の方が型情報の面で優秀。(errはunknown型)
ただし「try-catch」は「大域脱出」(関数の外へ脱出した後も脱出出来る=>複数ヶ所で発生する例外を一緒にまとめられる)

例外処理のfinallyにしか出来ないこと

例外の脱出に割り込める。またreturnにも割り込める。何がなんでも実行するものに使おう。

例外処理のthrowは何でも投げられるが...

Errorもしくはその継承を基本的には投げよう。エラーのデバッグの時にやっぱり便利。

try {
  hoge() // ここでエラーが発生
} catch (err) {
  if (err instanceof KumaError){
      console.log(err.message)
  } else {
      throw err
  }
}

※ちなみになんでも投げられるからこそ、catchの引数のerrはunknown型となっている
※ReactはPromiseオブジェクトをthrowしてコンポーネントのサスペンドを表すらしい...?

declare

declareはTypeScriptに特有の構文であり、以下のように関数や変数の型を中身なしに宣言できる構文です。

declareの例
declare function foo(arg: number): number;

クロージャ(closure)

関数の中にデータが内包されているもの

ブルーベリー本

『プロを目指す人のためのTypeScript入門』読者が最新情報にキャッチアップできる記事

Kumamoto-HamachiKumamoto-Hamachi

クラス関連の話

constructor

super呼び出しは必ずしもコンストラクタの一番先頭じゃなくても良いけど、thisにアクセスするより先に行わなきゃダメ!

class Hoge implements 型 {...}

Kumamoto-HamachiKumamoto-Hamachi

JSの話

thisとアロー関数

普通はthisは呼び出し元を参照

// これはundefinedに
関数()

// これはobjに
obj.関数()

アロー関数はthisを外側の関数から受け継ぐ。(自身のthisを持たない)
これがあるおかげで外側のthisをいったんthatとかにいれて使う...とか言うテクニックいらなくなって楽だなぁ〜

関数の中以外のthis

トップレベルのスコープでのthis:undefined
「classのプロパティ宣言の中」と「コンストラクタ内」のthis:new時に作られるインスタンス

applyとcall

applyとcallの違いはapplyが第二引数を配列にまとめる方でcallは第二第三引数で連ねていく。

// 関数funcをthisをobjとして呼び出す
// apply
func.apply(obj, args)
func.apply(obj, [])
func.apply(obj, [1, 2, 3])

// call
func.call(obj, args)
func.call(obj)
func.call(obj, 1, 2, 3)

Reflectオブジェクト

Reflect(グローバル変数)はcallはなくapplyのみ。

Reflect.apply(func, obj, [args])

bind

TODO:関数の引数を予め固定する機能

// 「呼び出し時のthisがobjに固定されたfunc関数」が返り値として得られる
const bindedFunc = func.bind(obj)

nullとundefiend

undefinedとnullの違い | TypeScript入門『サバイバルTypeScript』

Kumamoto-HamachiKumamoto-Hamachi

ユニオン型とインターセクション型

ユニオン型は|「または」。インターセクション型は&「かつ」。

でも実際は「オブジェクト型を拡張した新しい型を作る」という用途で使われることが多い。

A = B & CならAはBにとってもCにとっても部分型となる。

「または」を満たすための「かつ」 TODO:型の絞り込みの話

「関数型同士のユニオン型を作り、それを関数として呼び出す場合、引数の型としてインターセクション型が現れる」(by プロになるためのTS本)

type Ship = {
    full_draft: number; // 満載喫水
}
type Airplane = {
    flight_speed: number; // 飛行速度
}

type getShipSpec = (ship: Ship) => number;
type getAirplaneSpec = (airplane: Airplane) => number;
const getVehicleSpec: getShipSpec | getAirplaneSpec = (vehicle: Ship | Airplane): number => {
    if ('full_draft' in vehicle) {
        return vehicle.full_draft;
    }
    return vehicle.flight_speed;
}
const KumaShip: Ship = {full_draft: 300}

getVehicleSpec(KumaShip)

getVehicleSpecはifで条件分岐しているし気持ちとしてはShipでもAirplaneでもどっちでもええやんとなるが、ダメ。

TSの気持ちとしては「getVehicleSpecgetShipSpecなのかgetAirplaneSpecなのかわからん」から。気持ちとしては下記のようなコードと同じ。

const getShipSpec = (ship: Ship) => ship.full_draft;
const getAirplaneSpec = (airplane: Airplane) => airplane.flight_speed;
// 確率的にどちらの関数か決まる(ユニオン型)
const getVehicleSpec = Math.random() > 0.5 ? getShipSpec : getAirplaneSpec;

const KumaShip: Ship = {full_draft: 300}
const KumaAirplane: Airplane = {flight_speed: 1000}
const KumaVehicle: Ship & Airplane = {
    ...KumaShip,
    ...KumaAirplane
}

// error: TS2345: Argument of type 'Ship' is not assignable to parameter of type 'Ship & Airplane'.
getVehicleSpec(KumaShip)
getVehicleSpec(KumaAirplane)

// これはOK
getVehicleSpec(KumaVehicle)

関数型同士のユニオン型の呼び出し

「関数型同士のユニオン型の呼び出し」の際に

  • 引数の型はどうなるか?
    =>インターセクション型となる!なぜなら反変の位置にあったから!

  • 返り値の型はどうなるか?
    =>ユニオン型となる!なぜなら共変の位置にあったから!

オプショナルチェイニング(optional chaining)

obj?.propみたいなやつ。あとはhogeFunc?.()とかも。
?はそれ以降のプロパティアクセス・関数呼び出し・メソッド呼び出しをまとめて飛ばす効果を持つ。

type GetName = () => string;
const getName = (func: GetName | undefined): string | undefined => {
    return func?.();
}

コントロールフロー解析(control flow analysis)、型の絞り込み TODO

  • typeof演算子

  • 代数的データ型(ADT:algebraic data types、tagged union、直和型)の再現
Kumamoto-HamachiKumamoto-Hamachi

4種類のリテラル型(literal type)

リテラル型はプリミティブ型をさらに細分化した型

(1)文字列(2)数値(3)真偽値(4)BigInt

ユニオン型と一緒に使うとべんり。

type Foo = "foo";
const foo: Foo = "foo";
const bar: Foo = "bar"

// こう書くことも出来る(あまりやらないが)
const foo: "foo" = "foo"
function sign(type: "number" | "string"){...

(プロになるためのTS本より)

テンプレートリテラル型 TODO

widening TODO

する時としない時

Kumamoto-HamachiKumamoto-Hamachi

部分型のお気持ち(記事用メモ)

部分型とは?

部分型(subtyping)とは2つの型の代替/代入可能性を表す概念・実装を示す。

もっと荒っぽく言うと「型Aが型Bの部分型である」、とは「型Aが型Bの上位互換」であるというのと同じだと言える。

// BusinessPersonはPersonの部分型
type Person = {
    name: string;
    age: number;
}
type BusinessPerson = {
    name: string;
    age: number;
    job: string;
}

// OK
const kumamoto = { name: "Kumamoto", age: 20, job: "Engineer" };
const person: Person = kumamoto

// NG: error TS2741: Property 'job' is missing in type '{ name: string; age: number; }' but required in type 'BusinessPerson'.
const taro = { name: "Taro", age: 30 };
const bzPerson: BusinessPerson = taro

共変・反変

変数と返り値
引数

ユニオン型にからめて

type Person = {
    name: string;
    age: number;
}

type stringOrPerson = "Hoge" | Person;

const hoge: stringOrPerson = "Hoge";
const person: Person = {name: "Hanako", age: 20};
const hanako: stringOrPerson = person

まとめ