コンテンツにスキップ

TypeScript

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. TypeScript とは

静的型付けのフロントエンド言語である。

tsconfig.json ファイルに基づいて、TypeScript ファイルをコンパイルし、JavaScript ファイルを作成する。

拡張子として、tstsx (TypeScript 内に JSX を実装できる) を使用できる。


02 型検証の仕組み

型検証の有無

TypeScript は静的型付け言語のため、コンパイル時に型検証 (変数、引数、返却値で指定された型に対して、渡される値の型が一致しているかを検証するプロセス) を実施する。

ほかの静的型付け言語にはない『トランスパイル』というプロセスがあるが、ここでは型を検証しない。

実行前のコンパイル 実行前のトランスパイル 実行時
型検証

トランスパイル

▼ コンパイル + トランスパイルの両方

function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

// 引数を渡さない場合、関数にはundefinedを渡すことになる
greet();
// トランスパイルされてJavaScriptになる
// undefinedは型不一致のため、コンパイルの型検証によってエラーになる
error TS2554: Expected 1 arguments, but got 0.

▼ トランスパイルのみ

function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

// 引数を渡さない場合、関数にはundefinedを渡すことになる
greet();
// トランスパイルされてJavaScriptになる
// undefinedは型不一致であるが、コンパイルの型検証がないため、エラーにならない
Hello, undefined!


02. 変数の宣言

let

記入中...


const

▼ const アサーション

const で宣言/代入された変数には、再代入できないようにする。

型がより複雑な配列やオブジェクトリテラルで使用すると便利である。

const obj = {
  const obj: {
    readonly name: "pikachu";
    readonly no: 25;
    readonly genre: "mouse pokémon";
    readonly height: 0.4;
    readonly weight: 6;
  }
  name: "pikachu",
  no: 25,
  genre: "mouse pokémon",
  height: 0.4,
  weight: 6.0,
} as const;


02-02. 型

プリミティブ型

▼ 文字列 (String)

let str: string = "hello";
str = 0; // これはエラーになる

▼ 数値 (Number)

let num: number = 0;
num = "0"; // これはエラーになる

▼ 巨大な数値 (bigint)

let big: bigint = 10n;
big = 0; // これはエラーになる

▼ 真偽値 (Boolean)

let bool: boolean = true;
bool = 1; // これはエラーになる

▼ Null

let n: null = null;
n = undefined; // これはエラーになる

▼ Undefined

let u: undefined = undefined;
u = null; // これはエラーになる

▼ シンボル (Symbol)

let sym: symbol = Symbol();
sym = ""; // これはエラーになる


オブジェクト型 (Object)

▼ 変数

const object: {
  name: string;
  age: number;
} = {
  name: "taro",
  age: 20,
};

▼ 参照記法

const obj = {
  foo: 123,
  bar: 456,
  "baz-qux": 789,
};

const key = "bar";

// ドット記法
// キー名が静的で変わらない場合に適している
const value1 = obj.foo; // 123

// ブラケット記法
// キー名が動的な場合や記号を含む場合に適している
const value2 = obj[key]; // 456
const value3 = obj["baz-qux"]; // 789

▼ Record による定義

Record を使用することで、オブジェクト型をわかりやすく定義できる。

const object: Record<string, string> = {
  name: "taro",
  age: "20",
};
const object: Record<string, unknown> = {
  name: "taro",
  age: "20",
  foo: 1,
  bar: [],
};


配列型

▼ 配列とは

可変的な要素数、順序は自由、型はすべて同じデータ型である。

▼ 変数

const strArray: string[] = ["a", "b", "c"];

strArray.push(0);
const numArray: number[] = [1, 2, 3];

numArray.push("a");

▼ 配列の走査

const numbers = [10, 20, 30];

const total = numbers.reduce<number>(
  (result, value) => {
    // 現在の合計値(result)に、現在の要素(value)を加算する
    return result + value;
  },
  // 合計の初期値を0に設定する
  0,
);

console.log(total); // 60


タプル型

▼ タプルとは

配列の一種である。

固定の要素数、順序は固定、型は自由なデータ型である。

▼ 返却値

const foo = (a: number, b: string): [number, string] => {
  return [a, b];
};

const result = foo(1, "Alice");


ジェネリクス型

▼ 変数

// string[] と同じ
const str: Array<string> = ["a", "b", "c"];
// (string|number)[] と同じ
const strOrNum: Array<string | number> = ["a", "b", 1, 2];
// string{} と同じ
const str: Map<string> = {a: "a", b: "b", c: "c"};

▼ 返却値

// Promise<型>
async function asyncFn(): Promise<string> {
  // 非同期処理
  return "executed";
}

console.log(await asyncFn());

▼ 型変数(ジェネリクス)

安全な any 型ともいえる。

型変数では、定義した時点で型が決まっていない。

コール時に型変数へ任意の型を推論で代入し、それに合わせた引数型と返却型の関数を定義できる。

// 最初の<T>    型変数を定義
// (value: T)  引数型で型変数を使用
// : T         返却値型で型変数を使用

const foo = <T>(value: T): T {
  // ...
}
// この時点では、引数型と返却値型は決まっていない
// 変数名はなんでもよく、単語でもいい
const foo = <T>(value: T): T {
  return value;
}

// 型変数に文字を代入すると、これを推論し、string型の引数型と返却値型を定義していたことになる
foo("a");

// number型の引数型と返却値型を定義していたことになる
foo(1);
// この時点では、引数型と返却値型は決まっていない
// 変数名はなんでもよく、単語でもいい
const foo = <T>(value: T): Promise<T> => {
  return value;
};

// 型変数に文字を代入すると、これを推論し、Promise<string>型の引数型と返却値型を定義していたことになる
foo("a");

// Promise<string>型の引数型と返却値型を定義していたことになる
foo(1);
// この時点では、型変数 (T、U) の型は決まっていない
// 変数名はなんでもよく、単語でもいい
const foo = <T, U>(key1: T, key2: U): Array<T | U> => {
  return [key1, key2];
};

// 型変数に文字を代入すると、これを推論し、string型の引数型と返却値型を定義していたことになる
foo<string, string>("a", "b");

// number型の引数型と返却値型を定義していたことになる
foo<number, number>(1, 2);

// boolean型の引数型と返却値型を定義していたことになる
foo<boolean, boolean>(true, false);

// 複数の型を組み合わせることもできる
foo<string, number>("a", 1);
const measureFunctionExecutionTime = async <T>(
  fn: () => Promise<T>,
): Promise<{result: T; executionTime: number; startAtTimestamp: string}> => {
  // 計測開始
  // 計測にはperformance.now()のほうが適切であるが、ISO形式に変換してタイムスタンプを取得する必要があるため、これに対応するDate.now()を使用する
  const startAt = Date.now();

  const result = await fn();

  // 計測終了
  const executionTime = Date.now() - startAt;
  const startAtTimestamp = new Date(startAt).toISOString();

  // 関数の処理結果と実行時間を返却する
  return {
    result: result,
    executionTime: executionTime,
    startAtTimestamp: startAtTimestamp,
  };
};

// この時点では、引数型と返却値型は決まっていない
// 変数名はなんでもよく、単語でもいい
const foo = <T>(value: T): Promise<T> => {
  return value;
};

// 型変数に文字を代入すると、これを推論し、fn: () => Promise<T> のTが決まる
measureFunctionExecutionTime(foo);


数値型

▼ 引数

const sum = (x: number, y: number) => {
  return x + y;
};

console.log(sum(1, 2));

console.log(sum(1, "2")); // Argument of type 'string' is not assignable to parameter of type 'number'.

console.log(sum(1)); // Expected 2 arguments, but got 1.

▼ 返却値

const sum = (x: number, y: number): number => {
  return x + y;
};


返却値なし

// 返却値がないという型
const logger = (): void => {
  console.log("log");
};


Error オブジェクト型

▼ ビルトイン

interface Error {
  name: string;
  message: string;
  stack?: string;
}

▼ 独自の Error オブジェクト型

ステータスコードに応じたエラーを継承すると、try-catch で扱いやすくなる。

基本的には、ビルトインの Error オブジェクトを継承する。

// Errorオブジェクトを継承した独自のErrorオブジェクト
class FooError extends Error {
  private readonly _name: string;
  private readonly _code: number;

  constructor() {
    super(`Fooでエラーがあります`);

    Object.setPrototypeOf(this, new.target.prototype);

    // 親のErrorオブジェクトのプロパティに設定
    this.name = "FooError";
    this._code = 400;
  }
}

プロパティを Error オブジェクトの外から渡す場合は、次のとおりで実装する。

// Errorオブジェクトを継承した独自のErrorオブジェクト
class FooError extends Error {
  private readonly _name: string;
  private readonly _code: number;

  constructor(message: string, code: number) {
    // message変数はErrorオブジェクトに渡す
    super(message);

    Object.setPrototypeOf(this, new.target.prototype);

    // 親のErrorオブジェクトのプロパティに設定
    this._name = "FooError";
    this._code = code;
  }
}

独自の Error オブジェクトでは、必ずしも、Error オブジェクトを継承する必要はない。

例えば、任意の Error オブジェクトと実行時間や開始時間をプロパティにもつ FailedResult クラスがある。

独自の Error オブジェクトのプロパティに実行時間や開始時間を入れればいいと考えるかもしれないが、外部ツールの任意の Error オブジェクトには外からこれらを設定できないため、整合性が合わなくなる。

一方で、Error オブジェクトのプロパティへ実行時間や開始時間を入れれば、任意の Error オブジェクトへ対応できるようになる。

export class FailedResult {
  // resultプロパティにはErrorオブジェクトが入る
  private readonly _result: unknown;
  private readonly _executionTime?: number;
  private readonly _startAtTimestamp?: string;

  constructor({
    result,
    executionTime,
    startAtTimestamp,
  }: {
    result: unknown;
    executionTime?: number;
    startAtTimestamp?: string;
  }) {
    this._result = result;
    this._executionTime = executionTime;
    this._startAtTimestamp = startAtTimestamp;
  }

  get result() {
    return this._result;
  }

  get executionTime() {
    return this._executionTime;
  }

  get startAtTimestamp() {
    return this._startAtTimestamp;
  }
}


不明/任意

▼ unknown

unknown を使用した場合、それ以降の処理では型を無視して処理する。

function foo(): string | unknown {
  try {
    // 何らかの処理

    return "success";
  } catch (error: unknown) {
    if (error instanceof Error) {
      // unknownを使用したため、Error型として暗黙的に処理される
      console.error(error.message);
    } else {
      // unknownを使用したため、Error型以外の型として暗黙的に処理される
      console.error("Unknown error:", error);
    }
    return error;
  }
}

▼ any

any を使用した場合、それ以降の処理では型を無視して処理する。

function foo(): string | any {
  try {
    // 何らかの処理

    return "success";
  } catch (error: any) {
    if (error instanceof Error) {
      // anyを使用したため、Error型ではなく型無しとして暗黙的に処理される
      console.error(error.message);
    } else {
      // anyを使用したため、型無しとして暗黙的に処理される
      console.error("Unknown error:", error);
    }
    return error;
  }
}


03. 型推論

暗黙的

let name = "John"; // 変数nameは文字列として推論されます
let age = 30; // 変数ageは数値として推論されます
let isProgrammer = true; // 変数isProgrammerはブール値として推論されます


明示的

let name: string = "John";
let age: number = 30;
let isProgrammer: boolean = true;


型アサーション

▼ 型アサーションとは

型を上書きする。

キャストではないらしい。

as 構文

指定した型に上書きする。

const value: string | number = "this is a string";
const strLength: number = (value as string).length;

▼ アングルブラケット構文

指定した型に上書きする。

const value: string | number = "this is a string";
const strLength: number = (<string>value).length;

▼ 非 null アサーション (!)

null を含まない型に上書きする。

例えば、変数の型が <任意の型> | null である場合に、<任意の型> に上書きする。

// string型またはnullを許容する
function foo(value: string | null) {
  // nullを含まないstring型に上書きする
  const stringNotNull = value!;
  console.log(stringNotNull.toUpperCase());
}

ただし、型ガードしてもよい。こちらのほうが明示的だが、実装量が増えてしまう点がデメリットである。

// string型またはnullを許容する
function foo(value: string | null) {
  if (value === null) {
    return;
  }
  // string型として扱われる
  const stringNotNull = value.toUpperCase();
  console.log(stringNotNull);
}


型ガード

複数の型を許容している場合に、if 文を使用して型を絞り込むこと。

より寛容な型 (any) ほど、型ガードのパターンが増える。

// string型またはnumber型を許容する
function foo(value: string | number) {
  if (typeof value === "string") {
    // string型として扱われる
    console.log(value.toUpperCase());
  } else {
    // 残りのnumber型として扱われる
    console.log(value.toFixed(2));
  }
}


複数の型

▼ プロパティで複数の型を許容

name プロパティは string 型と undefined 型を許容している。

undefined 型のほうが網羅範囲は広い。結果的に undefined 型として扱われる。

type User = {
  id: number;
  name: string | undefined;
};

const user1: User = {id: 1, name: "Alice"};
const user2: User = {id: "abc123", name: "Bob"};

▼ オブジェクト全体で複数の型を用意

name プロパティが string 型の User と、undefined 型の UserWithoutName を別々に定義している。

undefined 型のほうが網羅範囲は広い。結果的に undefined 型として扱われる。

type User = {
  id: number;
  name: string;
};

type UserWithoutName = {
  id: number;
  name: undefined;
};

const user1: User = {id: 1, name: "Alice"};
const user2: UserWithoutName = {id: 1, name: undefined};


04. 独自の型宣言

比較

type エイリアス宣言のほうが型としての強制力が高い。

また、interface 宣言はオブジェクト指向の文脈でメソッドの仕様を持たせることが多く、型の文脈では適さない(と個人的に思っている)

項目 type エイリアス宣言 interface 宣言
継承 基本はできない (交差型のみできる) できる
継承時の上書き 基本はできない (交差型のみできる) できる
同名の型 できない できる
Mapped Types 型 できる できない


type エイリアス宣言

▼ type エイリアス宣言とは

オブジェクト以外の型を宣言する場合、type エイリアス宣言を使用する。

ただ、オブジェクトで type エイリアス宣言を使用してもよい。

type Foo = {
  bar: number;
  baz: Date;
  qux: string;
};

▼ オプショナル型

プロパティがないことを許容する型である。

type A = {foo?: string; bar?: number};

// fooやbarが存在しない場合も許容する
function method(arg: A): void {
  const fooValue = arg.foo ?? "(fooなし)";
  const barValue = arg.bar ?? "(barなし)";
  console.log(`foo: ${fooValue}`, `bar: ${barValue}`);
}

// fooのみでもOK
method({foo: "FOO"});

// barのみでもOK
method({bar: 1});

// どちらもなくてもOK
method({});

▼ 交差型(Intersection)

複数の型を同時に満たす型である。

型を指定した処理では、両方の型を指定しないといけない。

type A = {foo: string};
type B = {bar: number};

// 複数の型を同時に満たす
type AB = A & B;

// 両方の型を指定する
function method(arg: AB): void {
  console.log(`foo: ${arg.foo}, bar: ${arg.bar}`);
}

▼ 合併型 (Union)

複数の型のいずれかを満たす型である。

型を指定した処理では、いずれかの型を指定しなければならない。

type A = {foo: string};
type B = {bar: number};

// 複数の型のいずれかを満たす
type AB = A | B;

function method(arg: AB): void {
  // いずれかの型を指定しないといけない
  if ("foo" in arg) {
    console.log(`foo: ${arg.foo}`);
    return;
  }

  console.log(`bar: ${arg.bar}`);
  return;
}


interface 宣言

オブジェクトの型を宣言する場合、interface 宣言を使用する。

ただ、オブジェクトで type エイリアス宣言を使用してもよい。

interface Foo {
  bar: number;
  baz: Date;
  qux: string;
}


05. 環境変数の定義

出力

▼ 言語の実行環境

  • export コマンドで出力する
  • コンテナの環境変数として出力する

▼ dotenv パッケージ

dotenv パッケージ

なお、依存パッケージが増えてしまうため、代替の方法があるならそちらのほうがよい。

import dotenv from "dotenv";

// .envファイルを読み込む
dotenv.config();

// なんらかの実装


型の定義

interface Env {
  DATABASE_NAME: string;
  DATABASE_PORT?: number;
}

const myEnv: Env = {
  DATABASE_NAME: process.env.DATABASE_NAME || "",
  DATABASE_PORT: process.env.DATABASE_PORT
    ? parseInt(process.env.DATABASE_PORT)
    : undefined,
};


06. パッケージ

import

▼ import とは

import {logger} from "./logger";

▼ 型と関数が混在

型と関数が混在する場合、いくつかの記法(型と関数で import を分けるなど)があるが、次の実装方法が一番よい。

import {
  foo1,
  foo2,
  foo3
  type foo4,
  type foo5,
  type foo6,
} from "./foo";

▼ 動的 import

一部のモジュールはサーダーレンダリング中に読み込むことができず、ファイルの上部で import するとエラーになることがある。

こういったモジュールは CSR モードのクライアントレンダリング

useEffect(() => {
  // react-apexchartsは内部でブラウザのwindowを参照するため、本ファイルの上部でimportするとSSRモードのサーバーレンダリング中にwindowを参照できずにエラーになる
  // 動的importを使用することで、react-apexchartsの部分だけをCSRモードのクライアントレンダリング中に読み込むようにする
  // @see https://github.com/apexcharts/react-apexcharts/issues/526
  import("react-apexcharts").then((module) => {
    let Component = (module as any).default;

    // 検証したところ、動的importで出力されるreactコンポーネントの型や構造はビルド方法(yarn dev、yarn build)や実行環境(ローカルPC、CI、AWS)ごとに異なっている模様
    // 例えば、ローカルPCでyarn buildした場合と、yarn buildの結果をAWSにデプロイした場合の間で、動的importの返却値型や構造が異なる
    // そこで、動的importの返却値に含まれるfunction型のComponentを確実に使用できるように、型と構造を検証する

    // Componentの構造に関する対処
    if (typeof Component !== "function") {
      // function型のComponentを再代入できたことを期待する
      Component = (Component as any)?.default;
    }

    // Componentの型に関する対処
    if (typeof Component !== "function") {
      // 再代入したComponentがfunction型でない場合、コンポーネントが想定外の型であるため、コンポーネントを表示しない
      return;
    }

    setChart(() => Component);
  });
}, []);


export

▼ export とは

export {logger} from "./logger";

▼ index.ts

各ディレクトリのエントリポイントとして使える。

index.ts ファイルで export しておくと、コールする側がディレクトリ単位でインポートできるようになる。

// utils/index.ts
export {logger} from "./logger";
export {logger} from "./errorHandler";

アスタリスクで一括エクスポートしてもよい。

// utils/index.ts
export * from "./logger";
export * from "./errorHandler";
// ファイルを個別に指定する必要がなくなる
import {fooLogger, fooErrorHandler} from "~/utils";