【TypeScript】type の使いどころが知りたい話
はじめに
※2020/04/12
お前の仕事はいつまで続くんだという話ですが、ようやく書き終わりました。。。
この記事は TypeScript Advent Calendar 2019 の 6 日目の記事です。
ふとコードを書いていて気になったのが、例えばサーバー側から受け取った値を JSON に変換したいとき、 class を使うべき? interface を使うべき? それとも type ?ということでした。
C# であれば class を選択することになります。
では TypeScript の場合は?
というか、 C# にも存在する class と interface はともかく、 type って何よ?どう使うの?となったので調べてみることにしました。
type について
まず type (正確には Type Aliases) とは何か、という話から。
alias (別名)の名前通り、型に対する別名をつけるためのものです。
使い方はこんな感じです。
type SampleType = string; let sample: SampleType = "hello"; type SampleType2 = { id: number, name: string, } let sample2: SampleType2 = { id: 0, name: "world", }
interface と type aliases
共通点
よく比較される interface と type aliases ですが、実際使い方によっては全く区別がつかない場合もあります。
interface SampleInterface { id: number; name: string; } let sample3: SampleInterface = { id: 1, name: "!!!", }
どちらも JavaScript への変換後は消えてしまうためあくまで TypeScript 内での振る舞いに留まる点や、同じプロパティを持っていればその interface / type aliases として扱われる点など。
よく言われる(?)のが、(type aliases は継承ができないため)継承が必要なら interface を使う、という内容。
ver.3.8.3 現在、Classが実装する(implements)場合はどちらも可能です。
interface ISample{ name: string; } type TSample = { message: string }; /** OK */ class IClassSample implements ISample { public name: string = 'Hello' } /** OK */ class TClassSample implements TSample { public message: string = 'World' }
interface / type aliases で違いはあるのか?その使い分けは?というのが気になったので調べてみることにしました。
(口調がいかがでしたかブログっぽいな)
違い
declaration merging
interface の大きな特徴(だと思う)は "declaration merging" です。
同じ namespace 、同じファイル、同じモジュール内に同名の interface がある場合、ひとまとまりの interface として扱われます。
interface ISample{ name: string; } interface ISample{ message: string; } function main() { // 1つ目、2つ目の ISample が統合される. const iSample: ISample = { name: 'Hello', message: 'World' }; }
同様のことは type alias で Cross 型を使って再現することはできますが、明示的に書く必要があります。
interface ISample{
name: string;
}
type TSample = {
message: string
};
type TCrossSample = ISample & TSample
function main() {
const tSample: TCrossSample = {
name: 'Hello',
message: 'World'
};
}
extends
これも interface のみの特徴ですが、 interface は interface や type aliases を拡張 (extends) できます。
interface ISample{
name: string;
}
type TSample = {
message: string
};
interface IExtendSample extends ISample, TSample {
id: number;
}
function main() {
const iSample: IExtendSample = {
id: 0,
name: 'Hello',
message: 'World'
};
}
これも同じく type alias で Cross 型を使って再現できます。
定義できる型
interface で定義できるのは下記のような形式のみです。
(propertyName はなくてもOK)
interface InterfaceName {
propertyName: Type
};
type aliases はあくまで型に別名をつけるだけ、ということもあってか、色々な種類の型を定義できます。
interface ISample{
name: string;
}
type TSample = {
message: string
};
/** Union type */
type TUnionSample = ISample|TSample;
/** Cross type */
type TCrossSample = ISample & TSample & { id: number; }
/** Tuple */
type TTupleSample = [string, number];
/** Function */
type TFunctionSample = (message: string) => void;
/** Object */
type TObjectSample = {
message: string
};
/** Mapped type */
type TBase = {
id: number,
name: string
};
type TMappedSample = { [P in keyof TBase]: string; };
/** Conditional type */
type TConditionalSample = T extends string? 'string': 'other';
function main() {
const crossSample: TCrossSample = {
id: 0,
name: 'Hello',
message: 'World'
};
const tupleSample: TTupleSample = ['hello', 0];
const mappedSample: TMappedSample = {
id: '0',
name: 'Hello'
};
const conditionalSample: TConditionalSample = 'string';
}
Union 型、 Tuple は type aliases のみで定義できるため、 interface は使えません。
いつ type aliases を使うべきか?
interface と type aliases の違いはわかった気がしますが、 ではいつ type aliases を使うか。
(どちらも使用可能な場合)
Effective TypeScript によると、プロジェクトのスタイルに合わせる・または "declaration merging" が必要かどうかで判断するべき、と。
個人的には C# に慣れていることもあり、振る舞い(メソッド)を定義して Class で実装する場合は interface 、Data Transfer Object の用に、データを格納するためのオブジェクトは type aliases を使いたい気がします。