🏬

【学習記録】TypeScriptのPartial型と関連型について

2023/01/16に公開

今年からTypeScriptを業務で使用することになりました。
コードを読んでいると、Partial型が使用されている箇所があったので、これを機会に学習しようと思いました。

学習に使用した教材はプロを目指す人のためのTypeScript入門です。

ユーティリティ型

Partial型はユーティリティ型の1つです。
ユーティリティ型(utility type)は、型から別の型を導き出してくれる型です。

Partial型

Partial型はオブジェクトを変換する型の一つです。
Partial<T>でTのすべてのプロパティをオプショナルにした型が作成されます。

partial.ts
type Human1 = Partial<{
  name: string;
  age: number; 
}>

const human1_1: Human1 = { name: "nao", age: 18 }
const human1_2: Human1 = { name: "nao" }	

Human1では、オプショナルなプロパティを宣言するための「?」の記述がありませんが、変数を定義する際にプロパティがなくてもコンパイルエラーは発生しません。

Partial型はこれだけですが、これを機会に本に記載されている他のユーティリティ型も試してみることにしました。

Require型

Partial型とは逆の意味になります。
Require<T>でTに与えられたオブジェクト型のすべてのプロパティをオプショナルではなくした型が作成されます。

required.ts
type OriginalHuman = {
  name?: string;
  age?: number;
}

type Human2 = Required<OriginalHuman>

// エラー
const human2: Human2 = { name: "nao" }

元のオブジェクト型では全てのプロパティがオプショナルになっていますが、
Requiredに渡すことによってオプショナルではなくなります。
上のコードではhuman2の変数を定義する際に「プロパティ 'age' は型 '{ name: string; }' にありませんが、型 'Required<OriginalHuman>' では必須です。」とエラーが発生します。

Readonly型

Tに与えられたオブジェクト型のすべてのプロパティを読み取り専用(readonly)にしたオブジェクト型が作成されます。

readonly.ts
type Human3 = Readonly<{
  name: string;
  age: number;
}>

const human3: Human3 = { name: "nao", age: 18}
// エラー
human3.name = "Taro"	

Human3ではname属性にreadonlyの記述はありませんが、name属性に再代入しようとすると
「読み取り専用プロパティであるため、'name' に代入することはできません。」というエラーが発生します。

Pick型

Pick<T, K>でTというオブジェクト型のうちKで指定した(Kの部分型である)名前の
プロパティのみを持つ新しいオブジェクト型がPickにより作られます。

pick.ts
type Human4 = Pick<{
  name: string;
  age: number; 
}, "age">

const human4_1: Human4 = { age: 30 }
// エラー
const human4_2: Human4 = { name: "nao" }	

Human4を定義する際に、age属性を指定しているため,human4_1では変数宣言はできていますが、
human4_2では「型 '{ name: string; }' を型 'Human4' に割り当てることはできません。
オブジェクト リテラルは既知のプロパティのみ指定できます。'name' は型 'Human4' に存在しません。」
というエラーが発生します。

Omit型

Pick型の逆です。
Omit<T, K>でTというオブジェクト型のうちKで指定した(Kの部分型である)名前の
プロパティ除いた新しいオブジェクト型が作成されます。

omit.ts
type Human5 = Omit<{
  name: string;
  age: number; 
}, "age">

const human5_1: Human5 = { name: "nao" }
const human5_2: Human5 = { age: 30 }	

human5_1では変数宣言はできていますが、
human5_2では「型 '{ age: number; }' を型 'Human5' に割り当てることはできません。
オブジェクト リテラルは既知のプロパティのみ指定できます。'age' は型 'Human5' に存在しません。」
というエラーが発生します。

Extract型

Extract<T, U>でTの構成要素のうちUの部分型であるもののみを抜き出した型を返します。
Tは通常ユニオン型です。

omit.ts
type Chaos = "nao" | 30 | true | null | undefined

type chaos_1 = Extract<Chaos, string>

このとき、chaos_1は"nao"のみをとる型となります。

Exclude型

Extract型の逆です。
Exclude<T, U>でTの構成要素のうちUの部分型であるもののみを取り除いた型を返します。

omit.ts
type Chaos = "nao" | 30 | true | null | undefined

type chaos_2 = Exclude<Chaos, string>

このとき、chaos_2は 30 | true | null | undefined 以外をとる型となります。

NonNullable型

Exclude<T, null | undefined>と同じ意味を表します。

omit.ts
type Chaos = "nao" | 30 | true | null | undefined

type chaos_3 = NonNullable<Chaos>

このとき、chaos_3は null | undefined 以外をとる型となります。

Record型

キーがKであり、プロパティがTであるオブジェクト型を作ります。

record.ts
type FruitNumbers_2 = Record< "apple" | "orange" | "strawberry", number >;

const fruitPrices_2: FruitNumbers_2 = { 
  apple: 120,
  orange: 80,
  strawberry: 50
}

ここから下は上述した本には記載されておりませんでしたが、
組み込み型としてTypeScriptに搭載されているので、合わせて記述します。

Partial型の実装を見てTypeScriptの勉強をする

ところで、上述した本には、次のような記載があります。

以下で解説される型はそれぞれ全然違う機能に見えるかもしれませんが、すべて裏ではmapped typesや
conditional typesを使用して実装されています。mapped typesやconditional typesがいかに> 応用性豊富かがわかりますね。逆に、これらの型を自分で実装することができれば、なかなか高度な
TypeScript力を持っていると言えるかもしれません。

自力で0から実装することは骨が折れるので、Partial型の実装を見てみたいと思います。

https://github.com/microsoft/TypeScript/blob/8586c995b57747bcc9abdfed901bea776644b5fb/src/lib/es5.d.ts#L1549-1551

こちらのGithubの1549~1551行目に定義されています。

es5.d.ts
type Partial<T> = {
    [P in keyof T]?: T[P];
};

一つずつ内容を見ていきます。

keyof型

オブジェクト型のキーで記述されているkeyofはkeyof型に関する記述です。
keyof型は、オブジェクト型からそのオブジェクトのプロパティ名の型を得る機能です。

keyof.ts
type Human = {
     name: string;
     age: number;
}
	
type HumanKeys = keyof Human;

この時HumanKeysは"name" | "age"というユニオン型になります。

lookup型

オブジェクト型のプロパティで記述されている右辺のT[P]はlookup型に関する記述です。
Tはオブジェクト型、Pは文字列のリテラル型です。
T[P]はTというオブジェクト型が持つPというプロパティの型となります。

lookup.ts
type Human = {
     type: "human";
     name: string;
     age: number;
};

// ageはnumber型
function setAge(human: Human, age: Human["age"]) {
  return {
   ...human,
   age
  }
}
	
const uhyo: Human = {
    type: "human",
  name: "uhyo",
  age: 26
}

const uhyo2 = setAge(uhyo, 27);

上のように Human["age"]と書くことで型情報を再利用することができます。

mapped types

引用文で触れられていたmapped typesです。
こちらは{ [P in K]: T }という構文で表されます。
意味としては「Kというユニオン型の各構成要素Pに対して、Pというプロパティが型Tを持つようなオブジェクトの型」です。

mapped_1.ts

type Fruit = "apple" | "orange" | "strawberry";

// Fruitの内容が一つずつ取り出され、number型を持つようになる
type FruitNumbers = {
  [P in Fruit]: number
}

const fruitPrices: FruitNumbers = {
  apple: 120,
  orange: 80,
  strawberry: 50
}

まとめ

以上の記述からもう一度Partialの定義を見ていきます。
具体例も合わせて記述します。

es5.d.ts
type Partial<T> = {
    [P in keyof T]?: T[P];
};

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

type OriginalPartial<Human> = {
  // Human[A]はプロパティの型名によって型が変化する
  [A in keyof Human]?: Human[A];
  // name: string | undefined
  // age: number | undefined
}

Humanのプロパティが1つずつ取り出されます。
OriginalPartial型の1つ目のプロパティはnameとなります。このプロパティはHumanの"name"の型を参照し、string型を持ちます。"?"の記述があるため、name: string | undefinedというユニオン型になります。そして、2つ目のプロパティは、age: number | undefinedとなります。

なお、{ [P in keyof T]?: U}(Tは既存の型引数、Uは任意の型)は、homomorphic mapped typeと呼ばれます。(次のURL参照)

https://zenn.dev/uhyo/articles/array-homomorphic-mapped-type

参考資料

本文中に記述した資料以外では次の資料を参考にしています。
サバイバルTypeScript

Discussion