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"; // コンパイルエラーになる

引用元
https://typescriptbook.jp/reference/values-types-variables/enum/enum-problems-and-alternatives-to-enums

メタグロス中島メタグロス中島

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のサブタイプであるとき、以下を満たしている

  1. 関数Aのthis型がしていされていない。もしくは関数Aのthis型が関数Bのthis型のサブクラスである
  2. 関数Aの引数が関数Bのスーパータイプである。つまり引数だけ反変である。
  3. 関数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