⌨️

【TS】公式ドキュメントから学ぶTypeScript(0. 最初の一歩編)

に公開

はじめに

  • ReactやNext.jsで良く出てくるから、TypeScriptを学ぼうと思いTypeScriptの公式ドキュメントを見たが、全部英語...
  • 「とりあえず入りの『Getting Start』を理解しないと始まらない!」そんな思いで筆者がドキュメントの内容を嚙み砕き、そこに独自で調べた内容を加えたものが、本記事の内容です。
  • 筆者はJSに触れたことがあるので、本記事の対象も自然とJSの経験者になる想定です、悪しからず...

1. TypeScriptとは

TypeScriptとはJavaScriptの機能はそのままに、Javaを始めとした静的コンパイラ言語のように型定義を行う機能を有したプログラミング言語です。
この型定義機能により、想定外の挙動を起こすコードをエラーとして明示したり、バグの発生頻度を減らすことができます。

2. interfaceによる型定義

TypeScriptでは、オブジェクト値の持つプロパティの型を予め定義するinterfaceという機能が存在しています。
値の種類で「オブジェクト」と「プリミティブ」の2つが存在しますが、あくまで「オブジェクト」を対象としてます。
次の例を見てみましょう。

以下のようにstring型の name とnumber型の id をプロパティとして持つuserオブジェクトを定義する場合、JavaScriptでは以下のように記載します。

const user = {
  name: "Hayes",
  id: 0,
};

これをTypeScriptの型定義機能、及び interface を使って、user オブジェクトがとるべき型(User)を定義できます。
また、この型(User)をオブジェクトに適用させるには、宣言直後に : User を記載します。

// User interface の宣言
interface User {
  name: string;
  id: number;
}

// userオブジェクトに User interface を適用
const user: User = {
  name: "Hayes",
  id: 0,
};

クラスやオブジェクト指向プログラミングを使用する場合も同様に型定義が可能です。

interface User {
  name: string;
  id: number;
}
 
class UserAccount {
  name: string;
  id: number;
 
  constructor(name: string, id: number) {
    this.name = name;
    this.id = id;
  }
}
 
const user: User = new UserAccount("Murphy", 1);

また、この interface は関数における引数や戻り値にも使用できます。
その場合は、以下のように記載します。

// 引数への適用
function deleteUser(user: User) {
  // ...
}

// 戻り値への適用
function getAdminUser(): User {
  //...
}

3. Typeによる型定義

TypeScriptでは、型定義を行う機能として interfaceのほかにtypeが用意されています。
interfaceではオブジェクト値の型指定しかできませんが、typeではプリミティブ値でも型(リテラル)の定義が行えます。
また、typeではinterfaceでは不可能な型演算により、「型同士の組み合わせ」や「条件ごとの型設定」ができます。(型演算の一つである Union は後述)

他にもinterfaceとの機能的な差がありますが、TypeScriptの公式ドキュメントから内容が大きく外れてしまうため、本記事では詳細な説明は割愛します。
「他にどんな型演算があるの?」「InterfaceとType、結局どっちを使えばいいの?」という疑問が沸いた場合、以下の記事がとても参考になります。

https://ar-aca.tech/posts/typescript-advanced-type-system/

https://zenn.dev/luvmini511/articles/6c6f69481c2d17

4. 型の結合

TypeScriptでは、様々な型を1つの型定義に纏める機能があります。
具体的には、1定義に複数の型(リテラル)を指定する Unions、型定義で指定する型そのものに変数を割り当てる Generics です。
以下で詳しく見ていきましょう。

Unions

Union を使用すると、定義する型が取りうる複数の型(リテラル)を指定することができます。
もちろん、number型(のリテラル)とstring型(のリテラル)というような、異なる型でUnionを行うことも可能です。

// 同じ型のリテラルでのUnion
type MyBool = true | false;
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;

// 異なる型のリテラルでのUnion
type numberOne = 1 | "1";

// 異なる型でのUnion
type typeSet = string | number;

unionという機能は関数の引数に割り当てることも可能です。
以下では、関数getLengthの引数objに対し string 型若しくは string 型の配列を指定しています。

function getLength(obj: string | string[]) {
  return obj.length;
}

Generics

Generics は所謂「可変の型」です。
予め可変の型を定義しておき、呼び出す際に「どの型に指定するか」を型引数(<>の中)で渡します。
以下は、JSで予め定義されている Arrayオブジェクトを、Generics を使用して呼び出し、変数に代入しています。

type StringArray = Array<string>; // 要素を string 型に指定
type NumberArray = Array<number>; // 要素を number 型に指定
type ObjectWithNameArray = Array<{ name: string }>; // 要素を object 型に指定し、かつ name プロパティを string 型に指定

また、Generics は次のように使用して型宣言を行います。
ここで、型パラメータ(<>の中)の命名は任意ですが、string や number など実際に存在する型の名前を指定することはできません。

// Generics を使用した型の定義
interface Backpack<Type> {
  add: (obj: Type) => void;
  get: () => Type;
}

// backback を宣言し、Backpack 型を指定。
// 型引数には string を渡す。( Type == string となる)
declare const backpack: Backpack<string>;

// getメソッドを実行し、objectに代入
// 上記で型引数に string を渡しているため、 object には string 型が代入される。
const object = backpack.get();

// addメソッドを実行
// 上記で型引数に string を渡しているため、addメソッドの引数は string 型でなければならない。
backpack.add('backpack');

5. 構造的型付け

TypeScriptでは構造的型付けを採用しています。
これはコードに明示的に記載せずとも、二つの対象を比較して同じ構造をしていれば、それらは同じ型であると認識する機能の事です。
ちょっと分かり辛いので、例を見てみましょう。

interface Point {
  x: number;
  y: number;
}
 
function logPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}
 
// logs "12, 26"
const point = { x: 12, y: 26 };
logPoint(point);

ここでは、変数pointの型をPointと宣言していないにも関わらず、関数logPointはエラーが出ることなく正常に実行されます。
これは、変数pointと型Pointを比較し、どちらも同じ構造していたため、Point型であると見なされたためです。

また、この構造的型付けには「ある変数が、型が要求するメンバ(プロパティ・要素など)を持っていた場合、その型とみなされる」ということです。
先ほどの例で言うと、変数にPoint型にあるxとyがありさえすれば、その変数はPoint型としてみなされます。

interface Point {
  x: number;
  y: number;
}

function logPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}

const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // logs "12, 26"
 
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // logs "33, 3"
 
const color = { hex: "#187ABF" };
logPoint(color); // ⇒ エラー!

上記の例ではオブジェクトを使用しましたが、配列・タプル・関数型・ジェネリクス など形を比較できるあらゆる複合型に適用されます。

おわりに

Genericsは良く見るけど、最初見た時「なんだこれ」ってなったなあ。
「Getting Start」の続きも書きたいな、時間と気力があれば...🫠

Discussion