Open5
『プログラミング TypeScript』を用いたTypeScript学びなおし
3章 型について
unknown (P21)
unknown型は前もって型がわからないとき用いる
let a: unknown = 30
let b = a === 123 // boolean
let c = a + 10 // error
if (typeof a === 'number') {
let c = a + 10 // ここで絞り込みができているためOK
}
object (P27)
objectはプロパティに過不足があってはならない
type Name = { firstName: string, lastName: string }
const a: Name = { firstName: "中島" } // プロパティ 'lastName' は型 '{ firstName: string; }' にありませんが、型 'A' では必須です。
const b: Name = { firstName: "中島", lastName: "メタグロス", middleName: "田中" } // オブジェクト リテラルは既知のプロパティのみ指定できます。'middleName' は型 'Name' に存在しません。
class Person {
constructor(
public firstName: string,
public lastName: string,
) { }
}
const c: Name = new Person("中島", "メタグロス") // これはOK
class Singer {
constructor(
public firstName: string,
public lastName: string,
public middleName: string,
) { }
}
const d: Name = new Singer("中島", "メタグロス", "田中") // なぜかこれもOK
Object型 (P30)
let danger = {} // 空のオブジェクトは危険。Object型に推論される。
danger = { apple: 3 }// OK
danger = 4 // OK
danger = [] // OK
danger = Symbol() // OK
danger = undefined // 型 'undefined' を型 '{}' に割り当てることはできません。
danger = null // 型 'null' を型 '{}' に割り当てることはできません。
合併型・交差型 (P33)
合併型はどちらでもOK、交差型はすべてのプロパティが必要。
| と & の意味で考えるとわかりやすい。
type Cat = { name: string, purrs: boolean }
type Dog = { name: string, barks: boolean, wags: boolean }
type UnionCatDog = Cat | Dog
type IntersectionCatDog = Cat & Dog
let tama: UnionCatDog = { name: "tama", purrs: true } // Cat型
let pochi: UnionCatDog = { name: "tama", barks: true, wags: false } // Dog型
let animal: UnionCatDog = { name: "tama", purrs: true, barks: true, wags: false } // Both
// 交差型はひとつでもプロパティがかけていたらerror
let tanuki1: IntersectionCatDog = { name: "tama", purrs: true, barks: true } // error
let tanuki2: IntersectionCatDog = { name: "tama", purrs: true, wags: true } // error
let tanuki3: IntersectionCatDog = { name: "tama", barks: true, wags: true } // error
let tanuki4: IntersectionCatDog = { name: "tama", purrs: true, barks: true, wags: true } // OK
const foo = (animal: UnionCatDog) => {
// OK
console.log("私の名前は", animal.name, "!!")
// error
// プロパティ 'purrs' は型 'UnionCatDog' に存在しません。
// プロパティ 'purrs' は型 'Dog' に存在しません。
console.log(animal.purrs)
// 型ガード
if ("purrs" in animal) {
console.log(animal.purrs) // OK
}
}
列挙型 (P44)
列挙型は落とし穴が存在するためv5以前では利用を控える
// TypeScript v4.9.5
enum ZeroOrOne {
Zero = 0,
One = 1,
}
const zeroOrOne: ZeroOrOne = 9;
enum StringEnum {
Foo = "foo",
}
const foo1: StringEnum = StringEnum.Foo; // コンパイル通る
const foo2: StringEnum = "foo"; // コンパイルエラーになる
引用元
4章 関数について
可変長引数
レストパラメータを用いることで、任意の数の引数を安全に受け取れるようになる。
const sum = (...number: number[]) => {
return number.reduce((total, v) => total + v, 0)
}
sum(1, 2, 3, 3) // OK
sum([2, 3, 4, 5]) // error
ジェネリック型エイリアス
ジェネリック型の宣言はエイリアスの直後。
利用するときは明示的に宣言する。
type MyEvent<T> = {
target: T
}
const myEvent: MyEvent<HTMLButtonElement | null> = {
target: null
}
制限付きポリモーフィズム
type TreeNode = {
value: string
}
type LeafNode = TreeNode & {
isLeaf: true
}
type InnerNode = TreeNode & {
children: [TreeNode] | [TreeNode, TreeNode]
}
// <T extends TreeNode> と書くことで、戻りもInnerNodeやLeafNodeの型が保持される
const mapNode = <T extends TreeNode>(
node: T,
func: (value: string) => string
) => {
return {
...node,
value: func(node.value)
}
}
const leafNode: LeafNode = { value: "a", isLeaf: true }
const mappedLeafNode = mapNode(leafNode, (v) => v.toUpperCase())
console.log(mappedLeafNode.isLeaf) // true
6章 高度な型(前半)
サブタイプとスーパータイプ
サブタイプの例
- タプルは配列のサブタイプ
- 配列はオブジェクトのサブタイプ
- すべてのものはanyのサブタイプ
- neverはすべてのもののサブタイプ
スーパータイプの例
- 配列はタプルのスーパータイプ
- オブジェクトは配列のスーパータイプ
- anyはすべてのスーパータイプ
- すべてはneverのスーパータイプ
共変性について
以下の例では、LegacyUserが期待される型のスーパータイプであるためエラーが表示される。
逆にNewUserは期待される型のサブタイプであるためエラーとならない。
これを共変性と言う。
type ExistingUser = {
id: number
name: string
}
type LegacyUser = {
id?: number | string
name: string
}
type NewUser = {
name: string
}
const deleteUser = (user: { id?: number, name: string }) => {
delete user.id
}
const legacyUser: LegacyUser = {
id: 1,
name: 'John'
}
/**
* Argument of type 'LegacyUser' is not assignable to parameter of type '{ id?: number | undefined; name: string; }'.
Types of property 'id' are incompatible.
Type 'string | number | undefined' is not assignable to type 'number | undefined'.
Type 'string' is not assignable to type 'number'.t
*/
// assignable: 割り当て可能
// incompatible: 互換性がない
deleteUser(legacyUser)
const newUser: NewUser = {
name: 'John'
}
deleteUser(newUser) // エラーにならない
関数の変性について
関数Aが関数Bのサブタイプであるとき、以下を満たしている
- 関数Aのthis型がしていされていない。もしくは関数Aのthis型が関数Bのthis型のサブクラスである
- 関数Aの引数が関数Bのスーパータイプである。つまり引数だけ反変である。
- 関数Aの戻り値が関数Bのサブタイプである
type SuperType = { value1: number, value2: number }
type SubType = { value1: number }
type FuncA = (arg: SuperType) => SubType
type FuncB = (arg: SubType) => SuperType
constアサーション
let a = { x: 3 } // { x: number }
let b: { x: 3 } = { x: 3 } // { x: 3 }
let c = { x: 3 } as const // { readonly x: 3 }
a.x = 4 // OK
b.x = 4 // Error: Type '4' is not assignable to type '3'.
c.x = 4 // Error: Cannot assign to 'x' because it is a read-only property.
tag付き合併型
type UserTextEvent = {
value: string
}
type UserMouseEvent = {
value: [number, number]
}
type UserEvent = UserTextEvent | UserMouseEvent
const handle = (event: UserEvent) => {
if (typeof event.value === 'string') {
event.value.toUpperCase() // OK 絞り込みの確認
return
}
event.value[0] = 100 // OK
}
型が複雑になるとうまくいかなくなる。
type UserTextEvent = {
value: string
target: HTMLInputElement
}
type UserMouseEvent = {
value: [number, number]
target: HTMLElement
}
type UserEvent = UserTextEvent | UserMouseEvent
const handle = (event: UserEvent) => {
if (typeof event.value === 'string') {
event.value.toUpperCase() // OK
// Error: Property 'value' does not exist on type 'HTMLInputElement | HTMLElement'.
// Property 'value' does not exist on type 'HTMLElement'.
event.target.value // !? HTMLElementじゃないのに…。
return
}
event.value[0] = 100 // OK
}
これを回避するためにタグ付けを行う。
タグはリテラル型の一意なタグを付ける。
type UserTextEvent = {
tag: 'text'
value: string
target: HTMLInputElement
}
type UserMouseEvent = {
tag: 'mouse'
value: [number, number]
target: HTMLElement
}
type UserEvent = UserTextEvent | UserMouseEvent
const handle = (event: UserEvent) => {
if (event.tag === 'text') {
event.value.toUpperCase() // OK
event.target.value = "test" // OK
return
}
event.value[0] = 100 // OK
}
6章 高度な型(中盤)
ルックアップ
キーを指定して値を取得することができる。これはすごい!
type APIResponse = {
user: {
userId: string
friendList: {
count: number
friends: {
firstName: string
lastName: string
}[]
}
}
}
type FriendList = APIResponse['user']['friendList']
const renderFriendList = (friendList: FriendList) => {
// レンダリングする処理
}
// [number]で配列型にアクセスすることを示す。
type Friend = FriendList['friends'][number] // { firstName: string; lastName: string }
const renderFriend = (friend: Friend) => {
// レンダリングする処理
}
keyof演算子
keyofを使うと、オブジェクトすべてのキーを文字列リテラルの合併型として取得できる。
type APIResponse = {
user: {
userId: string
friendList: {
count: number
friends: {
firstName: string
lastName: string
}[]
}
}
}
type User = keyof APIResponse // "user"
type friendList = keyof APIResponse["user"]["friendList"] // "friends" | "count"
// keyofを使うことでこんなこともできる
const get = <O extends object, K extends keyof O>(obj: O, key: K) => {
return obj[key]
}
const response: APIResponse = {
user: {
userId: "123",
friendList: {
count: 100,
friends: [{ firstName: "John", lastName: "Doe" }],
},
},
};
get(response, "user")
get(response.user, "userId")
get(response.user.friendList, "count")
get(response.user, "count") // Argument of type '"count"' is not assignable to parameter of type '"userId" | "friendList"'.
Record
レコード型を使うことで、特定のキーのすべてを強制することができる。
type OgerponType = "Grass" | "Fire" | "Water" | "Rock" // オーガポンのタイプ
type Item = "Kamado" | "Ido" | "Ishizue" | "midori"
// Property 'Rock' is missing in type '{ Grass: "midori"; Fire: "Kamado"; Water: "Ido"; }' but required in type 'Record<OgerponType, Item>'.
const ogerponAndItem: Record<OgerponType, Item> = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
}
マップ型
基本的にはレコード型と同じことができる。
type OgerponType = "Grass" | "Fire" | "Water" | "Rock" // オーガポンのタイプ
type Item = "Kamado" | "Ido" | "Ishizue" | "midori"
// Property 'Rock' is missing in type '{ Grass: "midori"; Fire: "Kamado"; Water: "Ido"; }'
// but required in type '{ Grass: Item; Fire: Item; Water: Item; Rock: Item; }'
const ogerponAndItem: { [key in OgerponType]: Item } = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
}
// OK: 省略可能にできる
const ogerponAndItem: { [key in OgerponType]?: Item } = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
}
// null 許容にできる
const ogerponAndItem: { [key in OgerponType]: Item | null } = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
Rock: null,
}
// readonlyにもできる
const ogerponAndItem: { readonly [key in OgerponType]: Item } = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
Rock: "Ishizue"
}
// Cannot assign to 'Grass' because it is a read-only property.
ogerponAndItem.Grass = "Ishizue"
// 必須にする
const ogerponAndItem: { [key in OgerponType]-?: Item } = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
Rock: "Ishizue"
}
// 書き込み可能にする
const ogerponAndItem: { -readonly [key in OgerponType]: Item } = {
Grass: "midori",
Fire: "Kamado",
Water: "Ido",
Rock: "Ishizue"
}
組み込みのMap型
type OgerponType = "Grass" | "Fire" | "Water" | "Rock" // オーガポンのタイプ
type Item = "Kamado" | "Ido" | "Ishizue" | "midori"
type OgerponAndItemBase = { [key in OgerponType]: Item }
type OgerponAndItemBuildIn1 = Record<OgerponType, Item>
// オプショナルはPartialで書き換え可能
type OgerponAndItem2 = { [key in OgerponType]?: Item }
type OgerponAndItemBuildIn2 = Partial<OgerponAndItemBase>
// 読み取り専用はReadonlyで書き換え可能
type OgerponAndItem3 = { readonly [key in OgerponType]: Item }
type OgerponAndItemBuildIn3 = Readonly<OgerponAndItemBase>
// ピックはPickで書き換え可能
type OgerponAndItem4 = { [key in "Grass" | "Fire"]: Item }
type OgerponAndItemBuildIn4 = Pick<OgerponAndItemBase, "Grass" | "Fire">
ユーザー型定義ガード
このように戻り値によって、引数の型をTypeScriptに伝えることができる。
// arg is stirng => 戻りがtrueのときはstring / falseのときはstringではない
const isString = (arg: unknown): arg is string => {
return typeof arg === "string"
}
const foo = (arg: string | number) => {
if (isString(arg)) {
console.log(arg.toUpperCase())
}
}
6章 高度な型(後半)
条件型
スーパータイプであるか否かを判定し、その結果によって動的に型を定義することができる。
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true型
type ToArray<T> = T[]
type A = ToArray<number> // number[]
type B = ToArray<number | string> // (string | number)[]
type ToArray2<T> = T extends unknown ? T[] : T[]
type A2 = ToArray2<number> // number[]
// number[] もしくは string[] のいずれかになる。合併型が分配される!!
type B2 = ToArray2<number | string> // number[] | string[]
// Tには含まれているがUには含まれていない型を抽出する
type Without<T, U> = T extends U ? never : T
type A3 = Without<number, string> // number
type B3 = Without<number | string, number> // string
// Without<number | string, number> は string になる理由
// 1. number extends number ? never : T => never
// 2. string extends number ? never : T => string
// 3. number | string extends number ? never : T => never | string
// 4. 結果、never | string となる。
// 5. never | string => stirng
inferキーワード
条件型ではジェネリック型をインラインで宣言することができる。
infer...推測する。
// T[number]はルックアップ型であることに注意
type ElementType<T> = T extends unknown[] ? T[number] : T
type A = ElementType<number> // number
type B = ElementType<number[]> // number
type C = ElementType<number | string> // number | string
type D = ElementType<number[] | string[]> // number | string
type E = ElementType<number[] | string> // number | string
// inferキーワードで書き直し・infer U の部分で新しく型を宣言している
type ElementType2<T> = T extends (infer U)[] ? U : T
type A2 = ElementType2<number> // number
type B2 = ElementType2<number[]> // number
type C2 = ElementType2<number | string> // number | string
type D2 = ElementType2<number[] | string[]> // number | string
type E2 = ElementType2<number[] | string> // number | string
組み込みの条件型
// Exclude(除外する)
type A = string | number | boolean
type B = string | boolean
type C = Exclude<A, B> // number
type Without<T, U> = T extends U ? never : T // これと等価
// Extract(抽出する)
type A2 = string | number | boolean
type B2 = string | boolean
type C2 = Extract<A, B> // string | boolean
type Within<T, U> = T extends U ? T : never // これと等価
// NonNullable(nullとundefinedを除外する)
type MyNonNullable<T> = NonNullable<T>
type A3 = string | number | undefined
type B3 = MyNonNullable<A3> // string | number
// ReturnType(関数の戻り値の型を取得する)
type A4 = () => string
type B4 = ReturnType<A4> // string