個人的に忘れがちなTSの基礎文法に関わるなぜのメモ
インデックス・シグネチャ=>代替策としての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 astring
before indexing into an object. That means that indexing with100
(anumber
) is the same thing as indexing with"100"
(astring
), 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
contextual typing(文脈に基づく型付け)
- 普通の推論:「代入する式」に付与される引数や返り値の型から「代入される変数」の型を推論
- contextual typing:「代入される変数」の型注釈から「代入する式」の型を推論
TODO
分割代入(配列・オブジェクト)
配列での分割代入
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
}
コールシグネチャ
TODO
プロパティの定義
TypeScript: Documentation - More on Functions
複数のコールシグネチャを持たせて関数オーバーローディングっぽいのを楽に実現
type SwapFucn = {
(arg: string): number;
(arg: number): string;
}
※参考:プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで (Software Design plus) | 鈴木 僚太 4.2.5 コールシグネチャによる関数型の表現
部分型関係(特に引数の話)
反変
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
部分型(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でしょ?」と気を利かせてくれているようだ。
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)
部分型 返り値
部分型 その他
関数型のメソッド記法
TODO:なぜゆるくなる?
読み取り専用
普通の配列は読み取り専用配列の部分型
オブジェクト型だとTSの不完全な部分が...
部分型の気持ち(自己理解)
要は上位互換。AがBの部分型であるとき
受け取る側がAを期待しているのにBを出したら「これ下位互換じゃねーか💢」となるし、受け取る側がBを期待していてAを出したら「おまけしてくれてありがと(使わんからどうでも良い情報あるけど)」となる。
引数の反変の話もこれで考えると...(TODO)
ジェネリクス
『型引数を受け取る関数を作る機能』のこと(BY プロを目指すためのTYPESCRIPT入門)
型引数を持つ型をジェネリクス型と呼ぶ。
type Kumamoto<T> = {
name: string;
job:T;
}
TypeScript: Documentation - Generics
ジェネリクス(ジェネリックプログラミング)とは - 意味をわかりやすく - IT用語辞典 e-Words
【TypeScript】Generics(ジェネリックス)を理解する - Qiita
配列とジェネリクス
T[]
はArray<T>
の省略。
雑多な話
取りうる値
値をデータの源としたい時は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'
例外処理と型
「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)
関数の中にデータが内包されているもの
ブルーベリー本
クラス関連の話
constructor
super呼び出しは必ずしもコンストラクタの一番先頭じゃなくても良いけど、thisにアクセスするより先に行わなきゃダメ!
class Hoge implements 型 {...}
tsconfig.json関連
noImplicitOverride
override修飾子使わないとオコ
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
ユニオン型とインターセクション型
ユニオン型は|
「または」。インターセクション型は&
「かつ」。
でも実際は「オブジェクト型を拡張した新しい型を作る」という用途で使われることが多い。
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の気持ちとしては「getVehicleSpec
はgetShipSpec
なのか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、直和型)の再現
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
する時としない時
keyof型
lookup型
ユーザー定義型ガード TODO
補足 Extract
部分型のお気持ち(記事用メモ)
部分型とは?
部分型(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