iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

型をもう少し自在に使える様になりたい

はじめに

こんにちは!木村です!

普段TypeScriptを使用しているのですが、型の理解がまだまだだなと感じるこの頃です。

基本的な型を意識して実装することはあれど、発展した型の指定が難しいなと思うことが多いので、一度きちんと勉強しようと思い今回の記事を書きました!

よろしくお願いいたします!

基本の型

number型

数値を扱う型。整数から浮動小数点数、負の値まで、あらゆる数値が分類されます。 NaN(Not a Number)やInfinityと言った特殊な値もnumber型に含まれます。

string型

テキストデータを表現するための型です。シングルクォート(’)、ダブルクォート(”)、またはバックティック(`)で囲んだ文字列を扱えます。

リテラル

リテラル:「文字通り」「字義通り」

const hoge = 'リテラル型'
let foo:string;
foo = hoge 

定数(const)で定義したhogeに文字列を入れた時、hogeの型はstring型ではなく、代入した値の文字列で型が定義されます。特定の値しかとることが出来ない型はリテラル型と呼ばれます。

上の例の場合、hogeはstring型をさらに限定した型、とみなされているので、string型として定義されたfooへの代入が許されます。

boolean型

真偽値を表現するための型。trueとfalseの二つのリテラル値。

ユニオン型

union:結合

複数の型を含む型です。

let user : string | number

Array型

配列に対応する型です。型に[] をつけて表します。

const stringArray: string[]
const numberArray: number[]

Tuple型

配列型に似ていますが、固定された長さを持ち、各要素に対して特定の型が指定されているものです。

const tuple: [string, number] = ['こんちには', 5]

上記の変数tuple に3つ目の要素を入れようとするとエラーになります。

また、Tapple型を使用する際は、Array型と区別をつけるために明示的に型注釈で型を指定する必要があります。

any型

any型の変数には型チェックが適用されず、any型の変数に任意の値を代入でき、またany型の変数を他の任意の型の変数に代入することも可能です。

型の恩恵がなくなるので基本的に使用を避けるべきですが、JSからTSへの移行を行う際に一時的に使用する場合や、外部APIを使用した際のレスポンスデータの型が不明な場合など、明確な目的があるときに使用します。

unknown型

any型と同じくどんな型の値でも代入することが可能ですが、こちらの方が型安全性を損なうことなく扱えます。

unknown型の変数を、別の型が期待される変数に直接代入することができません。

let value1: unknown = 1 
const value2: number = value1 //エラー
value1 + 1 //エラー
console.log(typeof value1 === 'number') //OK

また、unknown型に対しての算術演算もできません。

ただし、比較演算は許可されています。

つまり、未知の型に対して型の検証を要求することで、型を安全に使用できるようにしています。

オブジェクト型

オブジェクトに対応する型です。オブジェクト内の構造について定義することができます。

指定された型ではないプロパティを入れようとすると型エラーになります。

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

//OK
const obj: objType = {
  name: "hoge",
  age: 20,
};

//エラー
const obj2: objType = {
  name: "hoge",
  age: "20", //エラー
};

インデックス型

オブジェクトのフィールド名をあえて指定せず、プロパティの部分だけを指定したい際は、以下のように設定できます。

keyの部分は、任意の名前にできますが、「key」や「K」が一般的に使用されます。

type object = {
    [key: string]: number
}

Mapped Types

インデックス型では、(例の場合)string型であればどんな値も入れられるようになっています。特定の値でキーを設定したいようなときにはMappedTypesの指定の仕方が推奨されていました。

主にユニオン型と組み合わせて、キーの制約として使用できる仕組みです。

type PersonPropertyType = "name" | "age" | "tel" ;

type PersonType = {
    [key in PersonPropertyType]: string | number
}

const person1: PersonType = {
  name: "田中太郎",
  age: 20,
  tel: 1234567890,
};
  • 追加のプロパティ定義ができません。

      type PersonType = {
          [key in PersonPropertyType]: string | number,
          address: string; //エラー
      }
    

ジェネリクス

typescriptを始めてから慣れてきたし、typeチャレンジに取り組もうかな〜と軽い気持ちで挑んだときにいきなり出てくるこいつに一蹴された方、割といらっしゃると信じてます。

ジェネリクスgenerics):包括的な、全体的な、汎用な

簡単にいえば、その型が使用されるときに初めて型が固定される仕組みです。

例えば、以下のgetSortedArray のような、不明な型が入った配列を受け取ることができる関数があったとします。 この変数は、number型でもstring型でも受け取って並べ替えてくれます。いろんな型を受け取って並べ替えて欲しいのでany型にして実装してみました。 しかし、これだと返り値がany型になってしまいます。型ごとにこの関数を作るのも面倒だし、呼び出し元で型チェックを行うのも面倒です。

function getSortedArray(array: any[]) {
  return array.sort();
} 

const array = [2, 5, 4, 6, 1, 4, 3];
const array2 = ["a", "c", "b"];

const result = getSortedArray(array); //result: any[]
const result2 = getSortedArray(array2); // result2:any[]

こういったときに嬉しいのがジェネリクス型です。受け取った型で型推論をしてくれるので、stringの配列を渡したときはstringが、numberを渡したときはnumberが返り値の型になってくれます。

function getSortedArray<T>(array: T[]) {
  return array.sort();
}

const array = [2, 5, 4, 6, 1, 4, 3];
const array2 = ["a", "c", "b"];
const array3 = [2, 5, 1, "b", "a"];

const result = getSortedArray(array); //result: number[]
const result2 = getSortedArray(array2); // result2:string[]
const result3 = getSortedArray(array3); // result3: (string | number)[]

ジェネリクスを利用するときには、型パラメータ(型変数)を使用します。

上の例の、関数パラメータの丸括弧の前にある<T> がそうです。

型パラメータは任意の名前を設定することができますが、単一の大文字(T,U,Vなど)が一般的に使用されます。

ジェネリックエイリアス

エイリアス

改めて言われると「何?」となりましたが、以下のようなtypeキーワードで定義された型のことです。

type hogeType = string | number | undefined;
type PersonType = {
    name: string,
    age: number,
    tel: number[],
}

型パラメータを設定した型は、使用するときに任意の型を入れて使用することができます。

type Pair<T, U> = {
    first: T;
    second: U;
}

const person : Pair<string, number> ={
    first: 'hello',
    second: 123,
}

extends

ジェネリクス型ではどのような型でも入れることはできますが、そこまで柔軟だとちょっと都合悪いかな…という場合が出てくると思います。

例えば、以下のように配列の最初の要素を返す関数を作ったとき、ジェネリクスでどのような型の配列でも受け取れるようになっています。

これだと、const array = ['a', 1, undefined] のような配列でも受け取ることができるのでちょっと不便です。

function firstItem<T>(arr: T[]): T {
  return arr[0];
}

そういったときに、ジェネリクスの部分で、受け取ることができる型を指定することができます。

function firstItem<T extends number | string>(arr: T[]): T {
  return arr[0];
}

firstItem([1, 2, 3]); // 1
firstItem(["a", "b", "c"]); // "a"
firstItem(['a', 1, undefined]) //エラー

keyof 演算子と extends

keyof演算子

指定されたオブジェクト型から、そのプロパティキーのリテラル型を抽出し、それらを結合したユニオン型を生成します。

要はそのオブジェクトのキーの名前で型を作ることができるものです。

type PersonType = {
  name: string;
  age: number;
  tel: number[];
};

type PersonKey = keyof PersonType; // "name" | "age" | "tel"

// 型を直接出力できないので、新しく作成する変数にkeyofで抽出したリテラル型を当てて
// 代入したときにエラーが出ないかで確認
const keys: PersonKey[] = ["name", "age", "tel"]; // OK
const keys2: PersonKey[] = ["name", "age", "hoge"]; // hogeでエラー

extendsと組み合わせる

このkeyof演算子を使って引数を取る関数を作りました。

オブジェクトと、そのオブジェクトのキーを渡すことで、その値を返してくれる関数です。

普通に引数keyをstringで受け取るより良さげに見えますが、telを指定した時の返り値の型がstring | number | number[] になっています。

type PersonType = {
  name: string;
  age: number;
  tel: number[];
};

const person1: PersonType = {
  name: "田中太郎",
  age: 20,
  tel: [1234567890],
};

function getProperty(obj: PersonType, key: keyof PersonType) {
  return obj[key];
}

const result4 = getProperty(person1, "tel"); // [1234567890]

型推論を行う流れとして、

  1. keyof PersonType を計算
    1. keyofはプロパティキーを結合したユニオン型にするので、 keyof PersonType = "name" | "age" | "tel"
  2. getProperty で型解析 →PersonTypeの全てのプロパティのユニオン型
    1. PersonType["name"] | PersonType["age"] | PersonType["tel"]
    2. string | number | number[]

っていうところまでは計算してくれますが、引数のkeyの型の情報を受け取っておらず計算できないので、返り値がstring | number | number[] になっています。

渡してるオブジェクトのtelnumber[]なのでnumber[]として返してほしいところです。

そこで、先ほどのextendsを組み合わせて関数を作り直してみます。

function getProperty2<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]; 
}
  1. ジェネリクス<T, K> で任意の型の引数2つを受け取れるようにする
    • T:オブジェクト
    • K:オブジェクトのキー
  2. K extends typeof T で、「Tで受け取ったオブジェクトの、プロパティキーのうちのどれか」に制約する

こうすると、返り値がnumber[] になっています。

ユーティリティ型

型を変換したり新しい型を生成するために使用する、組み込みのジェネリック型です。型のutils関数みたいなものだと思ってます。

Partial

指定された型の全プロパティを任意のプロパティ(オプショナルプロパティ)に変換した新しい型を生成します。

ちなみにこれの逆を行うのはRequired<T> です。

type PersonType = {
  name: string;
  age: number;
  tel: number[];
};

const person1: PersonType = {
  name: "田中太郎",
  age: 20,
  tel: [1234567890],
};

function update(person:PersonType, fieldsToUpdate?:Partial<PersonType>){
    return {...person, ...fieldsToUpdate}
}

Record<K, T>

キーの型がK, 値の型が T であるオブジェクト型を構築するユーティリティ型です。

type ConfigKeysType = "theme" | "language" | "showNotifications";

const appConfig: Record<ConfigKeysType, string | boolean> = {
  theme: "dark",
  language: "ja",
  showNotifications: true,
};

Pick<T, K >

既存の型Tからいくつかのプロパティを選択して、新しい型を構成します。

type PersonType = {
  name: string;
  age: number;
  tel: number[];
};

type unchangeablePerson = Pick<PersonType, "name" | "age">

他のユーティリティ型については、やーさんが詳しく解説している記事を出してくださっているので、ご参照ください!

tech.iimon.co.jp

さいごに

基本的な型の部分でもちゃんと調べると知らなかったことがあったのでいい機会になりました。ジェネリクスの部分は、パッと書くのはまだ難しいかもしれませんが型安全を意識して思い浮かべられるようにしたいです。

ちなみに、途中で少しこぼしてましたが、最初のほうの問題の↓こちらで全然わからなくて調べたことがこの記事のきっかけです。

チャレンジしたことがない方、ぜひチャレンジしてみてください!

https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.md

参考・引用

現場で使えるTypeScript詳解実践ガイド

https://typescriptbook.jp/reference/type-reuse/mapped-types

https://typescriptbook.jp/reference/type-reuse/mapped-types