vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【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 を使いたい気がします。

参照