thumbnail

【TypeScript】Genericsの基礎

2022/06/16

Genericsは抽象的な型引数を用意しておくことで、関数・クラス・インターフェイスを使用時に型を指定できるになる仕組みです。

Genericsはtypescriptのライブラリにはほぼ確実に用意されているので、自然と触ったことがある人も多いと思います。

覚えておくことは少ない割に使う場面は結構多いので、サンプルコードを交えて簡単に紹介します。

Genericsの基本的な使い方

Genericsは関数・クラス・インターフェースで使用可能です。

関数

function genericsSample<T>(arg: T): T {
  return arg;
}

// アロー関数の場合は↓
const genericsSample2 = <T>(arg: T): T => {
  return arg;
};

const a = genericsSample<number>(1);
console.log(typeof a); // -> number

const b = genericsSample<string>('test');
console.log(typeof b); // -> string

genericsSample<number>('test2'); // NG 型エラー

// Genericsでも型推論が効くので、Genericsの省略が可能な場合がある
const c = genericsSample('test3');
console.log(typeof c); // -> string

この関数のGenericsの活用例としてHTTPクライアントライブラリのaxiosなどがあります。 例えばaxiosのgetは本来どのようなが返ってくるかわからないので通常はany型でresponseが返ってくるのですが、Genericsを利用することで返ってくるreponseの型を指定することができます。

import axios from 'axios';

async function example() {
  // 通常はresponseの中身はany型
  const res1 = await axios.get('https://example.com');
  console.log(typeof res1.data); // -> any

  // Genericsを利用することでresponse中身の型を指定できる
  const res2 = await axios.get<number>('https://example.com');
  console.log(typeof res2.data); // -> number
}

class

classの場合のGenerics定義方法は以下のとおりです。

class Sample<T> {
  item: T;

  constructor(item: T) {
    this.item = item;
  }

  getItem(): T {
    return this.item;
  }
}

const sample1 = new Sample<string>('test');
const item1 = sample1.getItem();
console.log(typeof item1); // string

const sample2 = new Sample<number>(5);
const item2 = sample2.getItem();
console.log(typeof item2); // number

interface

interfaceの場合のGenerics定義方法は以下のとおりです。

interface IService<Entity> {
  create(): Entity;
}

class User {
  constructor(public name: string, public age: number) {}
}

class UserService implements IService<User> {
  create() {
    const newUser = new User('takashi', 28);
    return newUser;
  }
}

複数の型引数の定義

Genericsは複数定義が可能です。

interface Store<T, U> {
    item1: T;
    item2: U;
}

const store: Store<string, number> = { item1: 'りんご', item2: 5 };

Genericsに制約を課す

ここまでの使い方だとGenericsにはどんな型も指定できますが、場合によってはGenericsの型に制限を設けたいときもあります。 その場合はextendsを使用するとGenericsに制約を課すことができます。

このGenericsの制約、かなり有用です。 例えば、Genericsの制約を上手く使うことで、関数の引数の型や返り値の型を動的にすることが可能になります。

具体例を以下に載せます。

例)Objectのkey、valueの取得、変更

playground

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

let user: User = {
  name: 'takashi',
  age: 30,
};

// user propertyの変更
const changeUserProperty = (key: keyof User, value: any) => {
  user = {
    ...user,
    [key]: value,
  };
};

changeUserProperty('age', 28); // OK
changeUserProperty('age', '28'); // 型エラーにならず通ってしまう...

// user propertyを取得
const getUserProperty = (key: keyof User) => {
  return user[key];
};

// getUserPropertyのreturnはstring | number
// になるので キャストする必要がある...
const userName = getUserProperty('name') as string;
const userAge = getUserProperty('age') as number;

イマイチtypescriptの型システムを使えきれてない感じがしますが、 Genericsを上手く使うと...

playground

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

let user: User = {
  name: 'takashi',
  age: 30,
};

// user propertyの変更
const changeUserProperty = <T extends keyof User>(key: T, value: User[T]) => {
  user = {
    ...user,
    [key]: value,
  };
};

changeUserProperty('age', 28); // OK
changeUserProperty('age', '28'); // NG 型エラー

// user propertyを取得
const getUserProperty = <T extends keyof User>(key: T) => {
  return user[key];
};

// キャスト必要なし
const userName = getUserProperty('name');
const userAge = getUserProperty('age');
console.log(typeof userName); // -> string
console.log(typeof userAge); // -> number

このような感じで型引数を適度に抽象化することで、より型安全にコードを書くことができます。

Genericsにより型を動的にすることでキャストしなくてよくなる場合もあるので、是非Genericsを使いこなしてみましょう。

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

Profile

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

© 2022 mitsuru takahashi