🥦

ナメやがってこの型ァ!!超イラつくぜぇ~~~~~ッ!!

2023/11/09に公開
8

TypeScriptのイラつく型について書くぜぇ~

Lookup Type

まずはこれだァ

const colors = ["赤","青"] as const;
type Colors = typeof colors[number]; //"赤" | "青"

typeof colors[number]・・・ってよォ~~~~~

typeofはわかる。スゲーよくわかる
TypeScritの世界では、typeofで変数から型を作れるんだよなァ

だがnumberってのはどういう事だああ~~~~っ!?
配列にnumberを渡せるかっつーのよ───────ッ!!

ナメやがってこの型ァ!! 超イラつくぜぇ~~~~ッ!!

解説

これはよォ
Lookup Type」ってやつらしいぜぇ

たとえばよォ
👇みたいに書くとPerson型からプロパティの型を取り出せるよなァ~??

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

type name = Person["name"]; // string
type nameAndAge = Person["name" | "age"]; // string | number

こういうのを「Lookup Type」って呼ぶらしいんだけどよォ

「Lookup Type」を配列に応用するとよォ
👇みたいな感じで考えられるぜぇ

const colors = ["赤","青"] as const;
type Colors1 = typeof colors[0 | 1]; //"赤" | "青"
type Colors2 = typeof colors[number]; //"赤" | "青"

要するに、numberって書くとユニオン(0 | 1)に展開されるってイメージだなァ

keyof

次はこれだァ

const Person = {
  name: "yamada",
  age: 50
}
type PersonProperty =  keyof typeof Person; //"name" | "age"

keyofは「プロパティのキーをユニオンとして取り出す」ってだけだぜぇ

たとえばこのコードは、keyofだけで書くと👇みたいになるぜぇ

type Person = {
  name: string,
  age: number
}
type PersonProperty =  keyof Person; //"name" | "age"

分解して考えると簡単じゃねぇかァ──────ッ!!
なめてんのかァ───ッ!!このオレをッ!!
クソッ!!クソッ!!クソッ!!

コールシグネチャ

次はこれだァ

type Add = {
  bonus: number;
  (): number;
};

type Add・・・ってよォ~~~~~

bonusはわかる。スゲーよくわかる
Addというオブジェクトがあって、プロパティとしてbonusを持ってるって意味だよなァ~~

だが(): numberってのはどういう事だああ~~~~っ!?
プロパティに無名関数を渡せるかっつーのよ───────ッ!!

ナメやがってこの型ァ!! 超イラつくぜぇ~~~~ッ!!

解説

結論からいうと、これは「コールシグネチャ」っていう書き方だぜぇ

たとえばよォ~
以下のサンプルコードのaddtype Addに該当するぜぇ

function add(a: number, b: number) {
  return a + b + (add.bonus ?? 0);
}
add.bonus = 100;

console.log(add(1, 2)); // 103

addは「プロパティを持ってるし実行もできるぜ」なオブジェクトになってるよなァ~?

こういう「実行もできるぜ」なオブジェクトを表現したいときによォ
オブジェクト内に無名関数を書くと「実行もできるぜ」を表現できるってわけだァ

んで、この書き方のことを「コールシグネチャ」って呼ぶらしいぜぇ

でもオレはぜーんぜん納得いってねえぜぇ~
なんなんだよ『コールシグネチャ』って名前はよォ~~~ッ!!

なめてんのかァ───ッ!!このオレをッ!!
日本語で呼べ!! 日本語で!!
「実行もできるぜ記法」って呼べ!! チクショオ──────ッ!!

extends

次はこれだァ

type DarkModeCheck<T extends boolean> = T extends true ? "dark" : "light";

type LightMode = DarkModeCheck<false>; // "light"
type DarkMode = DarkModeCheck<true>; // "dark"

Tはわかる。スゲーよくわかる
これはジェネリクスってやつで、色々な型を設定できるんだよなァ

だがextendsが左辺と右辺にあるのはどういう事だああ~~~~~~っ!?
extendsは継承って意味じゃねぇのかよォ──────ッ!!

ナメやがってこの型ァ!!超イラつくぜぇ~~~~ッ!!

解説

extendsは👇の3つの意味を持つぜぇ

  • 型分岐
  • 型制約
  • 継承

それぞれ全然違うからよォ
順番に見ていくぜぇ

まず、さっきのコードを「型分岐」だけのコードに直してみるぜぇ👇

type DarkModeCheck<T> = T extends true ? "dark" : "light";

type LightMode = DarkModeCheck<false>; // "light"
type DarkMode = DarkModeCheck<true>; // "dark"

これだと意味が『分かる』よなァ~??

  • Ttrueだったら"dark"になる
  • Tfalseだったら"light"になる

「型分岐」は「三項演算子と同じ」って覚えると良いぜぇ

さらに、このコードに「型制約」を加えると👇になるぜぇ

type DarkModeCheck<T extends boolean> = T extends true ? "dark" : "light";

これで「booleanを満たす型じゃないとTは許可しないィィ───ッ!!」って意味にできるぜぇ

ここでextendsが2つ出てきて混乱すると思うからよォ
とりあえず👇で覚えるといいぜぇ

  • 型制約:左辺にextendsがある
  • 型分岐:右辺にextendsがある

あと最後の「継承」は、JavaScriptと一緒だなァ
たとえば👇みたいにして『継承』できるぜぇ

interface Animal {
  name: string;
}
interface Human extends Animal {
  think(): void;
}

ちなみに『型分岐』は「Conditional Type」って呼ばれることが多いからよォ
覚えとくといいかもなァ~

Distributive Conditional Types

次はこれだァ

type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>; //string[] | number[]

これはわかる。スゲーよくわかる
型分岐、もとい「Conditional Type」ってやつだよなァ

だが結果がstring[] | number[]になるのはどういう事だああ~~~~~~っ!?
(string | number)[]になるんじゃねぇのかよォ─────ッ!!

ナメやがってこの型ァ!!超イラつくぜぇ~~~~ッ!!

解説

これはマジで初見殺しなんだけどよォ

「Conditional Types」にユニオンを渡すと、分配されて計算される

っていう俺様ルールが存在するらしいぜぇ

この俺様ルールを「Distributive Conditional Types」と呼ぶぜぇ

そうだよなァ~??
意味わかんねェよなァ~??

要するに、これは👇のように計算されるんじゃなくて

type ToArray<Type> = (string | number) extends any ? Type[] : never;
                   = (string | number)[]

👇のように計算されるってことらしいぜぇ~

type ToArray<Type> = (string extends any ? Type[] : never) | (number extends any ? Type[] : never)
                   = string[] | number[]

こんなの初見で分かるわけねぇだろがァ──────ッ!!
クソッ!!クソッ!!クソッ!!

ちなみに最初のコードを👇に書き直すと、結果を (string | number)[]にできるぜぇ

> type ToArray<Type> = [Type] extends [any] ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>; // (string | number)[]

さらに意味がわかんねェぞォォォォ──────ッ!!
クソッ!!クソッ!!クソッ!!

infer

次はこれだァ

type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never;

type Res = MyAwaited<Promise<string>>; // string

「extends」はさっき『理解』したから分かるよなァ~~??

問題は<infer U>って部分だけどよォ

これは「型分岐の判定に使った型の一部を『型分岐』として使う」ができる機能だぜぇ

このコードの場合、「Promiseを継承している場合、Promiseの戻り値を『型分岐』として使う」ができるぜぇ

つまり、inferは『型分岐』とセットで使うことになるってことだなァ

infer と template literal

次はこれだァ

type MyCapitalize<S extends string> = S extends `${infer F}${infer Tail}`
  ? `${Uppercase<F>}${Tail}`
  : S;

type Hoge = MyCapitalize<"hoge">; // "Hoge"

これもマジで初見殺しだぜぇ

template literal の中でinferを2つ使うとよォ

「先頭1文字」と「残り」という分かれ方をする

・・・らしいぜぇ

つまり、 ${infer F}${infer Tail}FTailには、👇が入るってことだぜぇ

  • F:"h"
  • Tail:"oge"

こんなの初見で分かるわけねぇだろがァ──────ッ!!
クソッ!!クソッ!!クソッ!!

インデックスシグネチャ

次はこれだァ

type User = {
  [key: string]: string;
};
const user: User = {
  name: "yamada",
  email: "hoge@hoge.com"
}

このコードで新しく出てきてるのは[key: string]の部分だぜぇ

これは「インデックスシグネチャ」っていう機能で
「任意の型のプロパティを好きなだけ追加できる」ってやつだぜぇ

ちなみに、このkeyの部分は、好きな名前を付けれるぜぇ

Mapped Type

次はこれだァ

type User = {
  name: string;
  email: string;
};

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type Email = MyPick<User, "email">; // { email: string }

このコードで新しく出てきてるのは [P in K]の部分だぜぇ

この機能を「Mapped Type」って呼ぶぜぇ

Mapped Typeでは、「in演算子+ユニオン」でユニオンの数だけプロパティを作れるぜェ

たとえば、👇の3つは同じ意味だぜぇ

type User = {
  name: string;
  email: string;
};

type User = {
  [key in "name" | "email"]: string;
};

type User = Record<"name" | "email", string>;

要するに、「プロパティ名を[ ]で囲って、さらにinの右にユニオンを書くと、それらがプロパティ名として展開される」って感じだなァ

そんなわけでkeyofと組み合わせたのが最初のコード👇ってわけだなァ

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

ちなみに、インデックスシグネチャと似てるが、プロパティの数が👇のように違うぜぇ

  • インデックスシグネチャ:制限なし
  • Mapped Type:ユニオン分だけ

あとよォ
インデックスシグネチャでは他のプロパティも宣言できるけどよォ👇

type User = {
  [key: string]: string;
  hoge: string; //✅
};

「Mapped Type」では他のプロパティは宣言できねぇからなァ👇

type User = {
  [key in "name" | "email"]: string; 
  hoge: string; //❌
};

これは「なんで事前に分かってんのに1箇所に書かねえんだ この・・・ド低脳がァ───ッ!!」ってことだろうなァ

Key Remapping

最後はこれだァ

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;
         
//type LazyPerson = {
//    getName: () => string;
//    getAge: () => number;
//    getLocation: () => string;
//}

asを見ると「強制的に型をキャストするやつ」ってイメージがあるかもしれねェが
ここではまったく違う意味だぜぇ

最後のLazyPersonを見れば、どういう機能なのか『分かる』と思うけどよォ
これは「Mapped Type」にasをつけると別名を付けれるってやつだぜぇ

ほとんど使うことがねェと思うからよォ
「こういう文脈で出てくることもある」って程度に知っとけばいいんじゃねぇかァ~??

まとめ・・・ッ!!

「型付け」とはッ!!
 コードの荒野にッ!! 進むべき『道』を切り開くことだッ!!

おまえの命がけの「型付け」ッ!!
 ぼくは敬意を表するッ!!

第5部完

Discussion

YOSHIMURA YuuYOSHIMURA Yuu

勢いがおもしろかったですw ジョジョを知らないので、もしかしてそこからのネタだったら申し訳ないんですが

これはジェネリクスってやつで、動的に型を設定できるんだよなァ

ここの「動的」というのは(色々な意味があると思うですが)たとえばよくあるものだと

  • 動的リンク
  • 動的型付け

といった感じで「ランタイム」「実行時」という意味で使うことが多いと思います。一方でTypeScriptの型は実行時には消えてしまうと思うため、ここはたとえば「色々な型を設定できるんだよなァ」とかにしておくとより正確と思いました。

(誰かこれを記事と同じテンションに翻訳してほしい)

penpenpenpen

コメントありがとうございます。
たしかに「色々な型を設定できるんだよなァ」のほうが適切だと思いますので、そのまま使わせて頂きたいと思います。
ありがとうございます!🙇

michiharumichiharu

さらに意味がわかんねェぞォォォォ──────ッ!!
クソッ!!クソッ!!クソッ!!

超笑いました😆

r-sugir-sugi

知りたかった内容ばかりでした!
めちゃくちゃ分かりやすかったです🤓

michiharumichiharu

Pickではキーがサジェストされるのに Omit ではサジェストされないのはどういうことだァ~~??

type StrictOmit<T, K extends keyof T> = Omit<T, K>;

場合によっちゃァこれで Omit のイラつきから解放されるぜぇ