Katsushi OUGI: Code & Life Notes – Life goes on

投稿日:

TypeScriptの構造的型付け

Effective TypeScriptの読書メモ。

構造的型システムとは

JavaScriptの「ダックタイピング」はTypeScriptの世界では無用なモノだと思い込んでしまっていた。型を指定するのだから、曖昧なオブジェクトなど存在しない、と。
しかし、実際にはTypeScriptはこの「ダックタイピング」をいいかんじで利用していたのだ。この「いいかんじで利用」を「構造的型システム」と呼ぶ。
型に互換性があれば、同じ型と見なすことが可能。

interface Animal {
  bark: string;
  meow: string;
}

interface Dog {
  bark: string;
  meow: string;
  age: number;
}

function getAnimalBark(animal: Animal) {
  return animal.bark;
}

const dog: Dog = { bark: 'bark', meow: 'meow', age: 20 };

getAnimalBark(dog);

dogは型が違うからエラーになりそうなものだけど、型エラーは発生しない。構造的に互換性があるからだ。

TypeScriptの型はオープンである

こういう型エラーによく遭遇する。

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

function calculateLengthL1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];

    length += Math.abs(coord);
  }
  return length;
}

v[axis] の箇所で以下のような型エラーが発生する。

型 ‘string’ の式を使用して型 ‘Vector3D’ にインデックスを付けることはできないため、要素は暗黙的に ‘any’ 型になります。 型 ‘string’ のパラメーターを持つインデックス シグネチャが型 ‘Vector3D’ に見つかりませんでした。ts(7053)

  • axisはVector3D型であるvのキー。x, y, zのいずれかであるはず。
  • が!構造的型システムのルール上、別のプロパティが入り込んでいる可能性を否定できない

classによる構造的型付け

構造的型付けなのでこんなことも可能。

class SmallNumContainer {
  num: number;
  constructor(num: number) {
    if (num < 0 || num >= 10) {
      throw new Error(`You gave me ${num} but I want something 0-9.`);
    }
    this.num = num;
  }
}

const a = new SmallNumContainer(5);
const b: SmallNumContainer = { num: 2024 };

ユニットテストを書くときにめっちゃ便利

こんな関数があったとする。

interface Author {
  first: string;
  last: string;
}

function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT first, last FROM authors`);
  return authorRows.map((row) => ({ first: row[0], last: row[1] }));
}

これをテストする場合、PostgresDBのモックを作るのもよいが、構造的型付けを利用して限定的なインターフェースを定義したほうがもっと簡単だ

interface DB {
  runQuery: (sql: string) => any[];
}

function getAuthors(database: DB) {
  const authorRows = database.runQuery(`SELECT first, last FROM authors`);
  return authorRows.map((row) => ({ first: row[0], last: row[1] }));
}

test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [
        ['Toni', 'Morrison'],
        ['Maya', 'Angelou'],
      ];
    },
  });
  expect(authors).toEqual([
    { first: 'Toni', last: 'Morrison' },
    { first: 'Maya', last: 'Angelou' },
  ]);
});

getAuthorsの引数にPostgresDB型のオブジェクトを与えてもエラーにならない。構造的型付けだから!
これはとてもシンプルで良い実装だ。

まとめ

  • JavaScriptはダックタイピングを奨励し、TypeScriptはこれをモデリングするため構造的型付けを利用している。
  • インターフェースに代入可能な値は、型宣言に明示的に列挙されている以外のプロパティを持つ可能性がある。
  • クラスも構造的型付けの一部だ。
  • ユニットテストを簡単にするために構造的型付けを利用せよ。