【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の取得、変更
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を上手く使うと...
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を使いこなしてみましょう。