元組み込み系学部の新卒が、TypeScriptのユーザー定義型に立ち向かう

TypeScript

1. はじめに

アイスタイルAdvent Calender2021の13日目の担当します、新卒1年目のyakuratです。

今回書く内容は知っている方からすれば当たり前のことかもしれませんが、TS初学者且つこれまではC言語×μITRON×STM32のような環境で開発してきた私が受けたカルチャー(?)ショックと、そこから学んだ内容についてになります。

2. ユーザー定義型の定義方法

基本的にC言語を触ってきた私は、ユーザー定義型の定義には基本的に構造体を使っていました。

参考

/* C */
typedef struct {
    int id;
    char name[64];
    int price;
} product_info;

このような定義をTypeScriptでやろうとすると

interface ProductInfo {
  id: number,
  name: string,
  price: number
}

// OR

type ProductInfo =  {
  id: number,
  name: string,
  price: number
};

2通りで書けてしまいました。実装がばらけると嬉しくないですね。
業務では、interfaceをよく使うのですが、typeとは何が違うのか見ていこうと思います。

3. interface と type

物は試しに、typeとinterfaceの2通りでサンプルコードを動かしてみました。

interface ProductInfo {
  id: number,
  name: string,
  price?: number
}

function displayProductInfo(product: ProductInfo): void {
  console.log(`ProductId: ${product.id}`);
  console.log(`ProductName: ${product.name}`);
  console.log(`Price:${product.price}`);
}

const product: ProductInfo = {
  id: 1,
  name: 'product',
  price: 1000
};

const sample: ProductInfo = {
  id: 999,
  name: 'sample'
};

displayProductInfo(product);
displayProductInfo(sample);

type ProductInfo = {
  id: number,
  name: string,
  price?: number
};

function displayProductInfo(product: ProductInfo): void {
  console.log(`ProductId: ${product.id}`);
  console.log(`ProductName: ${product.name}`);
  console.log(`Price:${product.price}`);
}

const product: ProductInfo = {
  id: 1,
  name: 'product',
  price: 1000
};

const sample: ProductInfo = {
  id: 999,
  name: 'sample'
};

displayProductInfo(product);
displayProductInfo(sample);

以下、トランスパイルした結果と実行結果

// TS to JS
"use strict";
function displayProductInfo(product) {
  console.log(`ProductId: ${product.id}`);
  console.log(`ProductName: ${product.name}`);
  console.log(`Price:${product.price}`);
}
const product = {
  id: 1,
  name: 'product',
  price: 1000
};
const sample = {
  id: 999,
  name: 'sample'
};
displayProductInfo(product);
displayProductInfo(sample);

[LOG]: "ProductId: 1"
[LOG]: "ProductName: product"
[LOG]: "Price:1000"
[LOG]: "ProductId: 999"
[LOG]: "ProductName: sample"
[LOG]: "Price:undefined"

同じでした、どちらも変わりませんでした。
それもそのはず、トランスパイルされれば型定義は消えてしまいます。
こちらのを見ても、何が変わるのか分かりません。
参照:https://www.typescriptlang.org/docs/handbook/declaration-merging.html#basic-concepts
ならば、gccには怒られそうな無茶を試してみましょう。

4.Merging Interfaces

interface ProductInfo {
  id: number,
  name: string,
  price?: number
}

interface ProductInfo {
  isBargain?: boolean,
}

let productInSale: ProductInfo = {
  id: 1,
  name: 'product',
  price: 1000,
  isBargain: false // 初期化で後付けのisBargainが入れられました!
}

これですね、interfaceの強力なところは。正直見た瞬間思ったことはコンパイラに怒られそうということと、定義した型を自力で破壊しているのかなということでした。
同じファイル内に同じ名前で型定義をするとマージされます。
参照https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces

少なくともtype句でここまで自由に合成できる旨の記載は無く、明示的に合成して新しい名前の型を作る必要があります。

type ProductInfo = {
  id: number,
  name: string,
  price?: number
};

// typeを結合 パターン1
type SalesSeasonProductInfo = ProductInfo & {
  isBargain: boolean
};

let productInsale: SalesSeasonProductInfo = {
  id: 1,
  name: 'product',
  price: 1000,
  isBargain: false
}
type ProductInfo = {
  id: number,
  name: string,
  price?: number
};

type IsSalesSeason = {
  isBargain: boolean
}

// typeを結合 パターン2
type SalesSeasonProductInfo = ProductInfo & IsSalesSeason;

let productInsale: SalesSeasonProductInfo = {
  id: 1,
  name: 'product',
  price: 1000,
  isBargain: false
}

公式的にも型拡張にはinterfaceを使うことがおすすめされているので、今後の開発でもガンガン使っていこうと思います。

For this reason, extending types with interfaces/extends is suggested over creating intersection types.
参照:https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections

ただ、ユーザー定義型の型名には基本的に意味を込めるはずなので、乱用して何を定義しているのかわからない、という事態は避けねばならないなとも思います。

自作のライブラリとかをその場だけ拡張するみたいな使い方(呼び出し側を編集したくない且つ、定義を足したことをコードで分かるようにしたい)ができるのか?くらいの感覚なので、いい使いどころを見つけられるよう、精進します。

さいごに

ここまで読んでいただき、ありがとうございました!
TypeScriptのinterfaceが持つ型拡張の自由度には、良し悪しは置いておいて非常に驚かされました。
型付きの言語を窮屈そう、書き辛そうと感じている方に、そんなことなさそうと思っていただけたら幸いです。
それでは皆様、良いバックエンド開発ライフを!!

おまけ

C言語の構造体には型の合成はないのでやろうとすると…

#include <stdio.h>
#include <stdbool.h> /* c99からboolがライブラリに実装されていました */

typedef struct {
    int id;
    char name[64];
    int price; /* optionalはCでは聞いたことがありません.. */
} product_info;

typedef struct {
    product_info product; /* 構造体のネストと新しい型が必要 */
    bool is_bargain;
} sales_season_product_info;

void main(void) {
    char str[64];
    sales_season_product_info sale_product = {
        1,
        "product",
        1000,
        false
    };

    /* 表示まで */
    printf("商品ID:%d\n", sale_product.product.id);
    printf("商品名:%s\n", sale_product.product.name);
    printf("価格:%d\n", sale_product.product.price);
    printf("商品名:%d\n", sale_product.is_bargain);
}

明示的な型の指定が行数を増やし、ネストが呼び出す際の1行当たりの文字数を増やすなと、今見ると思います (´ཀ`」 ∠):

ちょっと懐かしいし毒づきながらも楽しんでるのは内緒

駆け出しWeb系エンジニアです! C言語とArduino、STM32で制御プログラミングやRaspberry PiでAPI叩いてIoTしていたところから一転、node&TypeScriptを使う環境に来ました。 いいな、いいな、型があるって良いな♪