エンジニア初心者がTypeScriptを学ぶ

TypeScriptの基礎


※作成中です。

Basic Types

: stringはアノテーションと呼び、変数の型を定義する。
他のprimitive typesも定義できる。

const firstName: string = 'Hey man';
const age: number = 23;
const fullAge: boolean = true;
const children: undefined = undefined;
const noValue: null = null;

以下が良くない例:

const message: string = 123;
// Error: Type 'number' is not assignable to type 'string'.

TypeScriptってなにを簡単に説明

TypeScriptは静的型付け言語(Statically typed language)
JavaScriptは動的型付け言語(Dynamically typed language)
TypeScriptはランタイムでは何もしない。
上記だとstring型の変数にnumberをアサインしてるのでエラーが出る。
だけど、ブラウザはTypeScriptではなくJavaScriptしか実行できない。
だから裏でTypeScriptをJavaScriptにコンパイルして、コンパイルで:stringなどのアノテーションを消してる。
ということは、上記の良くない例をJavaScriptにコンパイルしたら、ブラウザで実行は出来てしまう。
結論として、TypeScriptは開発時にエラーを型などのエラーを発見できるためできる言語。
後ほどもう少し詳しく説明する。

Type Inference

「型をいちいち指定してたらコードの量が増えてめんどくさい」と感じる人もいるかもだが、TypeScriptはtype inferenceということができる。
何かというと、型を「: string」みたいに定義しなくても、TypeScriptがあなたが書いた変数にアサインした値を見て自動的に型を予測してくれる。

const message: string = "こんにちは";

↓ほとんどの場合は変数を定義する際は普通のJavaScriptの書き方で型をいちいち指定しなくてよい。(Functionのパラメータやアーギュメントには定義する)

const message= "こんにちは";

TypeScriptはこの変数はstring型と分かってくれる。

TypeScriptを選ぶ理由

え、じゃあどうしてJavaScriptの代わりにTypeScriptを使用する必要があるのか?
メリットは以下通りで、特にリファクタリングしやすくなるという理由はデカいと思う。

  • コンパイル時に大量のバグやエラーをキャッチします
  • コードを読みやすく、保守しやすくする
  • コードベースのリファクタリングとスケーリングが容易になる

あとはTypeScriptを使うと、Intellisenseという機能で書いてるコードをAuto Completeしてくれて、実際にコードを書く量を少なくしてくれる?というメリットもある。

TypeScriptとは

tsc(TypeScript Compiler)とはTypeScriptコンパイラーの略で、TypeScriptをJavaScriptにコンパイルするツールのことを指す。
ほとんどのブラウザでtscは機能する。

const message: number = "3000";
// ↓コンパイルしたときに出てくるエラー
tsc:
/main.ts (1,7): Type 'string' is not assignable to type 'number'.

コンパイルが成功した場合にのみ、コードが実行されるみたい。
また、TypeScriptはNode.jsでもサポートされるようになり、バックエンド言語としても使えるようになった。

Any

TypeScriptでホットなトピックとしてよく聞くのがanyという型。
TypeScriptではanyでは何にでもなれるタイプのこと。
型のチェックをスキップしてしまうと同じ。
普通のJavaScriptを書いているのと同じ。
結論:できるだけ使わないのが良い。

const message: any = 1000;

Function Type Syntax

先ほどTypeScriptにはtype inferenceがあるから変数を定義する際は型をいちいち指定しなくても良いと書いたが、functionも同様か?
調べてみるとTypeScriptはfunctionのパラメーターやアーギュメントには定義した方が良いみたい。
理由はtype inferenceでもfunctionのパラメーターやアーギュメントの型を割り出す(infer)ことができないから。
ではreturnの型はどうか?賛否両論あるが、普通の変数と同様にTypeScriptが返り値の型をinferするので明示的に定義しなくて良い。

なのでこうではなく:

function makeMessage(name: string, a: number, b: number): string {
  return `${name} scored ${a + b}`;
}

このように書ける:

makeMessage(name: string, a: number, b: number) {
  return `${name} scored ${a + b}`;
}

Functionのreturn type(返り値の型)は定義しない方が良い理由としては、間違えて異なるreturn typeを定義してしまう可能性があるから。例えば、以下の例だとreturn typeは必ずstringだが、エンジニアが間違えて「: string | null」アノテーションでnullも定義してしまった場合、実際のreturn typeとは異なる(nullはreturnしない)ことになる。

makeMessage(name: string, a: number, b: number): string | null {
  return `${name} scored ${a + b}`;
}

Void

Returnしないfunctionの場合は、「: void」アノテーションがある。
JavaScriptはreturnしてないfunctionはデフォルトでundefinedをreturnする仕様だが、TypeScriptではvoidで明示的にこのfunctionはなにもreturnしない風に書ける。

function logSystemEvent(event: string, severity: "info" | "warning" | "error"): void {
  console.log(`SYSTEM ${severity.toUpperCase()}: ${event}`);
}

しかし、これもTypeScriptがinferできるので書く必要がない。
「: void」を消して、function名にカーソルを合わせると、以下が表示され明示的に「: void」を書かなくてもTypeScriptがこのfunctionのreturn typeはvoidとinferしてくれることが分かる。

Function Types

JavaScript(と TypeScript)では、関数は変数に代入できる値。
例としては以下。

// 通常の関数
function add(a: number, b: number) {
  return a + b;
}

// 変数に代入も可能
const sum = add;

// アロー関数
const multiply = (a: number, b: number) => a * b;

add も multiplyもfunctionで値になり、変数に代入できるし、他のfunctionに渡すこともできる。
TypeScriptでは、このようなfunctionもそれぞれ型を持っている。
そのfuntionの型というのは、return typeではなく、「このfunctionはどんな引数を受け取り、どんな戻り値を返すか」 を示すルール。

Return Type

function add(a: number, b: number): number {
  return a + b;
}

: number が return type
「このfunctionが返す値は number です」という情報だけ
引数の型は含まれない

Function Type

type BinaryOp = (a: number, b: number) => number;

引数と戻り値の型をまとめたもの
「このfunctionは数字2つを受け取り、数字を返す」というルール全体を表す
単に戻り値の型だけでは表せない情報も含まれる

さらにTypeScriptでは、functionの型を先に名前付きで定義しておくことができる。
先に型を定義しておくことで、同じ型のfunctionを複数作成するときに再利用できるためコードがスッキリして読みやすくなる。

// 「数字2つを受け取り、数字を返す関数」という型を事前定義
type BinaryOp = (a: number, b: number) => number;

// 通常の関数
const add: BinaryOp = function(a, b) {
  return a + b;
};

// 変数に代入した関数
const sum: BinaryOp = add;

// アロー関数
const multiply: BinaryOp = (a, b) => a * b;

Type Alias

TypeScriptでは、functionのパラメータには型を定義する必要があると学んだけど、パラメータが別のfunctionの場合も同様。

function setLoggerTimeout(
  loggerCallback: (firstMessage: string, secondMessage: string) => string,
  timeout: number,
) {
  // do something
}

しかしこうすると、引数が長くて読みずらいコードになってしまう。
そこで誕生したのがtype alias。

// typeキーワードを使って、type aliasを作成。
type LoggerCallback = (firstMessage: string, secondMessage: string) => string;

// 上記のような、2の引数で両方stringで、stringをreturnするfunctionを使うときは、型の定義として「LoggerCallback」と書ける。
function setLoggerTimeout(loggerCallback: LoggerCallback, timeout: number) {
  // do something
}

Importing Types

別ファイルやmodulesからtypeをインポートできる。

// 悪い例:typeをインポートしてるのかどうか分からない。
import { User, Post } from "./models";
// 良い例1:typeのみをインポートしてると一目で分かる。
import type { User, Post } from "./models";
// 良い例2
import { type User, type Post } from "./models";

Unions

Union Typeというのは、 | を使って、値が複数の型のいずれかになり得ることを表せる。

// productId は string か number のどちらか
let productId: string | number;

productId = "item_101";
productId = 101;
function getTicketInfo(id: string | number) {
  if (typeof id === "string") {
    const parseId = id.split("-")[1];
    const numberId = parseInt(parseId);
    return `Processing ticket: ${numberId}`;
  }

  return `Processing ticket: ${id}`;
}

余談だが、Go言語にはunion typeが存在しないらしい。

Optional Parameters

?キーワードでFunctionのパラメータは省略することもできる。

例:

function sendMessage(body: string, subject?: string): string {
  if (subject) {
    return `${subject}: ${body}`;
  }
  return body;
}

// 使用例
sendMessage("Hello!");           // subject は undefined
sendMessage("Hello!", "Info");   // subject は "Info"

省略可能なパラメータ を使う場合、2つのルールがあるらしい。

ルール1:必須パラメータの後に置く

// コンパイルエラー例
// Error: Required parameter cannot follow optional parameter
function sendMessage(subject?: string, body: string): void {
  // ...
}

ルール2:型には自動的に undefined が含まれる

省略可能パラメータは渡されなかった場合 undefined が入る とみなされる。
そのため、function内では型が指定した型 | undefined になる。

Default Parameters

以下のようにfunctionのパラメータにデフォルトの値を代入できる。
代入した場合は、type inferenceが効くので型を定義しなくてもよい。

function introduce(name: string, country = "Japan"): string {
  return `${name} is from ${country}`;
}

console.log(introduce("Alice"));
// Alice is from Japan
console.log(introduce("Bob", "Canada"));
// Bob is from Canada

Literal Types

通常のstring型やnumber型は、無限に多くの値を持つことができてしまう。
でも「とある引数は決められたパターンしか受け取らない」としたい場合に便利なのがリテラル型。

// 悪い例: string だと何でも入ってしまう
function orderLunch(menu: string) {
  console.log(`You ordered ${menu}`);
}

orderLunch("pizza");    // OK
orderLunch("ramen");    // OK
orderLunch("icecream"); // 実際は用意してないメニューでも通ってしまう!

ここでリテラル型を使うと、指定された文字列だけを受け付けるようにできる。

function orderLunch(menu: "pizza" | "ramen" | "salad") {
  console.log(`You ordered ${menu}`);
}

orderLunch("pizza");   // OK
orderLunch("ramen");   // OK
orderLunch("salad");   // OK
orderLunch("sushi");   // エラー: "sushi" は型に含まれていない

Value Unions

リテラル型は、パラメータに取れる値を特定の値だけに制限できたが、それをさらに便利にしたのがvalue unions。
Value unionで「複数の決められた値」だけを型として許可できるようにできる。

type CoffeeSize = "small" | "medium" | "large";

function orderCoffee(size: CoffeeSize) {
  console.log(`Ordering a ${size} coffee...`);
}

function getPrice(size: CoffeeSize): number {
  if (size === "small") return 300;
  if (size === "medium") return 400;
  return 500;
}

orderCoffee("medium"); // OK
console.log(getPrice("large")); // 500

型エイリアスにまとめることで、複数のfunctionで同じ型を使い回せる。

Super Set Unions

リテラル型やユニオン型を使うと、変数が取りうる値を限定できると分かった。
例えば「サイズは small / medium / large のみ」など。

でも、時には 「サイズは small / medium / large だけど、実際にはもっと広い範囲を受け入れる」 というケースもある。
そんなときに役立つのが Super Set Unions。

// 商品IDは実際にはどんな number でもOK
// ただし、よく使う商品が 101, 102, 103
type ProductID = 101 | 102 | 103 | number;

function getProductName(id: ProductID): string {
  switch (id) {
    case 101: return "Laptop";
    case 102: return "Smartphone";
    case 103: return "Headphones";
    default:  return `Unknown product #${id}`;
  }
}

開発者にとって「よく使う値」がすぐ分かるので、開発効率が向上する。

Template Literal Types

JavaScriptにはテンプレートリテラルという「文字列を埋め込みで組み立てる構文」がある。

const name = "Alice";
console.log(`Hello, ${name}!`); // Hello, Alice!

TypeScript ではなんと、このテンプレートリテラル構文を型定義でも使える。

type Method = "GET" | "POST" | "PUT" | "DELETE";

// エンドポイントを定義する型
type Endpoint = `/api/${Method}`;

// 展開されるとこうなる
// "/api/GET" | "/api/POST" | "/api/PUT" | "/api/DELETE"

const endpoint1: Endpoint = "/api/GET";     // OK
const endpoint2: Endpoint = "/api/DELETE";  // OK
const endpoint3: Endpoint = "/api/PATCH";   // エラー

これで「/api/ の後ろは必ず GET/POST/PUT/DELETE のどれか」というルールを型で表現できる。

Arrays

TypeScriptでは、配列にどんな要素が入るのかを型で指定できる。
一番よく使うのはブラケット([])を使った書き方

function printStudents(students: string[]) {
  for (const student of students) {
    console.log(`Student: ${student}`);
  }
}

printStudents(["Alice", "Bob", "Charlie"]);
// Student: Alice
// Student: Bob
// Student: Charlie

Type Parameters

TypeScriptでは配列の型を指定する方法が2パターンある。

([])を使う書き方

もっともよく使われる基本的な書き方

function registerScores(name: string, scores: number[]): void {
  console.log(`${name} has scores: ${scores.join(", ")}`);
}

Array を使う書き方

ジェネリック型を使った書き方

function registerScores(name: string, scores: Array<number>): void {
  console.log(`${name} has scores: ${scores.join(", ")}`);
}

両方とも「scores は数値の配列」という意味で、動作は同じ。

Heterogeneous Arrays

JavaScriptでは配列の中に異なる型の値を混在させることができるが、もちろんTypeScriptでも可能。

// string と number が混ざっているので型は (string | number)[]
let productList = ["Apple", 120, "Orange", 80];

function describe(item: string | number): void {
  if (typeof item === "string") {
    console.log(`商品名: ${item}`);
  } else {
    console.log(`価格: ${item}円`);
  }
}

productList.forEach(describe);
// 商品名: Apple
// 価格: 120円
// 商品名: Orange
// 価格: 80円

Go や Java のような静的型言語では配列に複数の型を混ぜるのは出来ないが、
TypeScriptでは「できるだけJavaScriptに寄せて、その上に型を載せる」 という思想があるため、こういった表現も可能。

Rest Parameters

Functionを定義するときに「引数の数が決まっていない」ケースがある。
そんなときに便利なのが Rest Parameters(レストパラメータ) 。

書き方はシンプルで、引数名の前に ...(ドット3つ)をつけるだけ。
Functionの中ではその引数が「配列」として扱えるようになる。

function reportScores(student: string, ...scores: number[]): string {
  return `${student} のスコアは: ${scores.join(", ")}`;
}

console.log(reportScores("Alice", 80, 90, 100));
// Alice のスコアは: 80, 90, 100

console.log(reportScores("Bob", 75, 60));
// Bob のスコアは: 75, 60

Rest Parameters と Spread Syntax の違い

よく混乱するのが Rest Parameters と Spread Syntax の違い。

Rest Parameters(関数定義側)
→ 複数の値をまとめて「配列」にする

Spread Syntax(関数呼び出し側)
→ 配列を展開して「個々の値」として渡す

function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

const values = [1, 2, 3];
console.log(sum(...values)); // 6

ここで
(...nums: number[]) が Rest Parameters
sum(...values) の ...values が Spread Syntax

Evolving Any

TypeScriptでは、空の配列を作ったときにちょっと面白い挙動になる。
それが 「Evolving Any(進化する any)」

まず、単純に空の配列を作ると TypeScript はこうinferする。

let items = [];
// items: any[]

最初は「どんな型でも入れられる配列」になっている。

値を入れると型が「進化」する。
ここで値を追加すると、その型に合わせて配列の型が更新される。

items.push("sword");
// items: string[]

items.push(100);
// items: (string | number)[]

最初は any[] だったのに、
文字列を入れたら → string[]
数字を入れたら → (string | number)[]
というふうに、配列の型が「進化」していく。

明示的に型をつけるとどうなる?

一方で、もし最初から型を指定していたら?

let scores: number[] = [];
scores.push(99);     // OK
scores.push("fail"); // エラー
// Argument of type 'string' is not assignable to parameter of type 'number'

当然「数値しか入れられない」配列になるので、文字列を入れようとするとエラーになる。

進化が止まるタイミング

「Evolving Any」は関数の内部など、スコープの中では柔軟に進化するが、その関数の外に値を返した瞬間に「確定」してしまう。

function getSettings() {
  let settings = [];
  // settings: any[]
  settings.push("dark-mode"); // string[]
  settings.push(3000);        // (string | number)[]
  return settings;
}

let config = getSettings();
// config: (string | number)[]

config.push(false); // エラー
// Argument of type 'boolean' is not assignable to parameter of type 'string | number'

関数内では「どんどん型が変わる」。
関数の外では「確定した型」になる。

Object Literal Types

Objectにも型を指定できる。

function logStudent(student: { name: string; score: number }) {
  console.log(`${student.name} scored ${student.score} points.`);
}

logStudent({ name: "Alice", score: 95 }); // Alice scored 95 points

型エイリアスを使って再利用することもできる。

type Student = {
  name: string;
  score: number;
};

function logStudent(student: Student) {
  console.log(`${student.name} scored ${student.score} points.`);
}

function congratulateStudent(student: Student) {
  if (student.score >= 90) {
    console.log(`Congratulations, ${student.name}!`);
  }
}

const alice: Student = { name: "Alice", score: 95 };
logStudent(alice);           // Alice scored 95 points
congratulateStudent(alice);  // Congratulations, Alice!

TypeScriptの素晴らしい所は、プロパティ名の打ち間違いや型の間違いをしたときにエディターで赤並み線が表示され教えてくれる。

Extra Properties

TypeScriptでは、functionにオブジェクトを渡すときにプロパティの数や型のチェックが入る。
以下がその基本ルール:
1. 必要なプロパティは必ず存在すること
2. 余計なプロパティは許されることもある(ただし例外あり)

type Product = {
  name: string;
  price: number;
};

const item = {
  name: "Laptop",
  price: 1200,
  warranty: "2 years", // 余分なプロパティ
};

function showProduct(product: Product) {
  console.log(`${product.name}: $${product.price}`);
}

// 変数を渡す場合は OK
showProduct(item); // Laptop: $1200

変数itemに余分なwarrantyプロパティがあっても、functionに渡すことができる。
「必要なプロパティが揃っていればOK」 という柔軟な挙動。

接オブジェクトリテラルを渡す場合

// 直接オブジェクトを渡すとエラーになる
showProduct({ name: "Laptop", price: 1200, warranty: "2 years" });
//  Error: Object literal may only specify known properties, and 'warranty' does not exist in type 'Product'.

オブジェクトリテラルを直接渡す場合、余分なプロパティがあるとエラーになる。
これはTypeScriptのExcess Property Checking(余分プロパティチェック) と呼ばれるルール。

Optional Object Properties

TypeScriptでは、オブジェクトのプロパティを必須ではなく任意にすることができる。
それがオプショナルプロパティ。
オプショナルプロパティを使うと、存在しても存在しなくても良いプロパティを表現できる。

type Product = {
  name: string;
  price: number;
  discount?: number; // discount は任意
};

discount? のように ? をつけるだけ
型は自動的に number | undefined になる

const item1: Product = { name: "Laptop", price: 1200 };
const item2: Product = { name: "Mouse", price: 25, discount: 5 };

item1にはdiscountがなくてもOK
item2にはdiscountがあってもOK

Function内での扱い

オプショナルプロパティは存在するかどうかを確認してから使う必要がある。

function showPrice(product: Product) {
  if (product.discount !== undefined) {
    console.log(`Price: $${product.price} (-$${product.discount} discount)`);
  } else {
    console.log(`Price: $${product.price}`);
  }
}

showPrice(item1); // Price: $1200
showPrice(item2); // Price: $25 (-$5 discount)

product.discount は number | undefined なので、存在チェックが必要。
これにより、実行時のエラーを未然に防げる。

※注意点
オプショナルプロパティは便利だが、必要なプロパティまでオプショナルにしすぎないこと。
必須のフィールドは必ず型に含めておくことで、余計なランタイムチェックを減らせる。

Empty Object Type

TypeScriptで空のオブジェクトを作ると、思わぬ挙動が起こることがある。

let newUser = {};
newUser.name = "Alice"; 
// エラー: Property 'name' does not exist on type '{}'

なぜエラーになるのか?
TypeScript は {} を 「プロパティが何も定義されていないオブジェクト型」として扱う。
そのため、後から name などのプロパティを追加することはできない。

なので、型を事前に定義してあげる。
後からプロパティを追加したい場合は、あらかじめ型を定義しておく。

type User = {
  name: string;
};

let newUser: User = {};
newUser.name = "Alice";

User 型を作ることで、どのプロパティが必須か明確にできる。
TypeScript が余計な代入やプロパティ追加を防いでくれる。

Discriminated Unions

TypeScriptでは、オブジェクトの型が複数ある場合、どの型を扱っているか判別するプロパティを持たせると便利。
このプロパティを discriminant property(識別子プロパティ) または タグ と呼びます。

type LoadingState = { status: "loading" };

type ErrorState = { 
  status: "error";    // Discriminant Property
  message: string;
};

type SuccessState = { 
  status: "success";  // Discriminant Property
  data: string[];
};

// 複数の状態のユニオン
type FetchState = LoadingState | ErrorState | SuccessState;

function handleFetch(state: FetchState) {
  switch (state.status) { // ← Discriminant Property を見て分岐
    case "loading":
      console.log("Loading...");
      break;
    case "error":
      console.log("Error:", state.message);
      break;
    case "success":
      console.log("Data received:", state.data);
      break;
  }
}

statusがdiscriminant property
この値を見るだけで「今どの型のオブジェクトか」が分かる。

Sets

Setは同じ値を1つだけ持つコレクション(重複なし) 。

// 文字列だけを入れる Set
const favoriteFruits = new Set<string>();

favoriteFruits.add("Apple");
favoriteFruits.add("Banana");

// favoriteFruits.add(42); 
// エラー: number は string に割り当てられない

Set と指定すると、文字列しか追加できない。
重同じフルーツを追加しても、重複は自動で無視される。

Setの便利なメソッド

  • add(value):値を追加する
  • delete(value):値を削除する
  • has(value):値が存在するか確認する
  • forEach(callback):全ての要素に対して処理をする
  • size:要素の数を取得する

配列からSetを作る

const fruits = ["Apple", "Banana", "Apple"];
const uniqueFruits = new Set<string>(fruits);

console.log(uniqueFruits);
// Set { 'Apple', 'Banana' }

Maps

Mapはキーと値のペアを管理するコレクションで、キーと値の型を指定できる。

const racerSpeeds = new Map<string, number>();
racerSpeeds.set("Mario", 120);
racerSpeeds.set("Luigi", 115);
console.log(racerSpeeds.get("Mario")); // 120
racerSpeeds.delete("Luigi");
console.log(racerSpeeds.has("Luigi")); // false
console.log(racerSpeeds.size); // 1

キーと値のペアを管理するコレクション。
<K, V> でキーと値の型を指定できる。
キーは文字列・数値・オブジェクトなど任意の型が使える。

Map の便利なメソッド

  • set(key, value):キーと値を追加・更新する
  • get(key):指定したキーの値を取得する
  • delete(key):指定したキーと値を削除する
  • has(key):指定したキーが存在するか確認する
  • forEach((value, key) => …):全てのキーと値に対して処理をする
  • size:要素の数を取得する

Dynamic Keys

Objectを作るとき、プロパティを全部先に決めておく必要があるってさっき言ったけど、実際は後から新たにプロパティを追加したいときもある。
たとえばお弁当箱をイメージすると、

type LunchBox = {
  rice: string;
  meat: string;
};

これは「ごはん」と「お肉」が必ず入っているお弁当。
今の状態だと、決まったメニューしか入れられない。

これが「Dynamic Keys(動的キー)」の世界。
つまり「何が入るか決まってないけど、好きに追加したい!」ってこと。

そこでindex signatureというのがある。

でも、なんでも入れていいとカオスになる、
「消しゴム」とか「靴下」とか。

そこで「入れていいもののルール」を作るのがindex signature。

たとえば:

type LunchBox = {
  [food: string]: string;
};

これは、
キー(food) → 料理の名前(文字列)
値 → 料理の中身(文字列)
というルール。

const myLunch: LunchBox = {
  rice: "white rice",
  meat: "fried chicken",
};

myLunch["dessert"] = "pudding";  // OK
myLunch["drink"] = "tea";        // OK
myLunch["spoon"] = 1;            // 数字はダメ!ルール違反

こうすると、

自由に「おかず」を増やせる
でも「値は必ず文字列」というルールは守れる
って感じ。

Dynamic Keys(動的キー)
→ 「オブジェクトのキーが固定じゃなくて、その時々で増えたり変わったりする」イメージ

Index Signature(インデックスシグネチャ)
→ 「でも、キーと値にはルールを決めておこうね」という仕組み。

「自由帳」だけど「クレヨンでしか描いちゃダメ!」みたいなもの。
これが Dynamic Keys + Index Signature

Dynamic Default Properties

Index signatureで自由にプロパティを増やせるのは便利だけど、特定のプロパティは必須にしたいときどうする?

たとえば、ログインフォームのデータ。
どんなに自由に項目を増やせるようにしたいとしても、email と password だけは必須にしたい。

そんな時はDynamic Default Propertiesという書き方がある。

type FormData = {
  [field: string]: string;
  email: string;
  password: string;
};

最初見たときは「ん? email と password はなくても [field: string]: string に含まれるじゃん」と思ったが、実は意味があるんです。
この型の意味はこう:
email と password は必ず必要
それ以外のプロパティは自由に追加できる(ただし値は文字列)

PropertyKey

ロッカーで考えるオブジェクトの鍵
オブジェクトって、言ってみれば ロッカー のようなもの。
「鍵(プロパティ名)」
「中身(値)」
学校のロッカーを例にすると、いろんな種類の鍵がある。
名前シール → "studentName"(string)
番号札 → 101(number)
秘密タグ → Symbol("specialAccess")(symbol)

これをまとめて扱えるのがPropertyKey。

TypeScriptでは、この「使える鍵の種類」をまとめた型がPropertyKey

type PropertyKey = string | number | symbol;
type LockerInfo = {
  [key: PropertyKey]: string | number | boolean;
};

const secretLocker: LockerInfo = {
  studentName: "Taro",            // stringキー
  101: "Math Locker",             // numberキー
  [Symbol("specialAccess")]: true, // symbolキー
  isOpen: false,                   // boolean値
};

Readonly Modifier

JavaScriptのconstは変数そのものを変更できなくするけど、
TypeScriptのreadonlyは、それのオブジェクト版。

普通のオブジェクトは自由に書き換え可能だけど、
readonlyをつけると、そのプロパティは一度設定したら変更できなくなる。

type Point = {
  readonly x: number; // 変更禁止
  y: number;          // 変更OK
};

const point: Point = {
  x: 10,
  y: 20,
};

x は readonly → 一度設定したら書き換え不可
y は普通 → 後から変更可能

“As Const” と Object.freeze

as const はTypeScript特有の魔法のような機能で、オブジェクトや配列を丸ごとreadonlyにできる。

const colors = ["red", "green", "blue"] as const;

// colors.push("yellow"); // エラー!

配列やオブジェクトの値が そのままリテラル型 になる。
追加・削除・書き換えは一切できない。

const config = {
  apiUrl: "https://api.cobrakai.com",
  admins: {
    johnny: "lawrence",
    daniel: "larusso",
  },
  features: ["no mercy", "not crying", "winning too much"],
} as const;

// config.apiUrl = "https://api.karate.com"; //  エラー
// config.admins.johnny = "larusso";         //  エラー
// config.features.push("sweep the leg");    //  エラー

JavaScriptのObject.freeze() はオブジェクトのトップレベルだけ凍らせるメソッド。

const frozenConfig = Object.freeze({
  apiUrl: "https://api.cobrakai.com",
  admins: {
    johnny: "lawrence",
    daniel: "larusso",
  },
  features: ["no mercy", "not crying", "winning too much"],
});

// frozenConfig.apiUrl = "https://api.karate.com"; //  エラー
frozenConfig.admins.johnny = "kreese";           //  OK
frozenConfig.features.push("sweep the leg");    //  OK

トップレベルは安全
ネストは普通に変更できる
TypeScriptはトップレベルの変更をコンパイルでチェックしてくれる。

Satisfies

type Role = "admin" | "user";

Role型は"admin"か"user"しか持てない。

as const だけの場合

const role2 = "guest" as const;

"guest"はそのままリテラル型"guest"になる。※string型ではない。なぜなら、string型だったら文字列ならだんでもOKだが、リテラル型は型は”guest”だけ。
TypeScript は「値は変わらないよ」と覚える(readonly 相当)。
でもrole2がRole型に合っているかは何も確認していない。

satisfies を使うとどうなるか

const role3 = "guest" as const satisfies Role; //  エラー

"guest"がRole型に合うかどうかをチェック。
Role型に"guest"はないので、ここでエラー。

as const は 値を固定するだけ → 型チェックはしない
satisfies は 型に合ってるか確認する → 値が型に合わなければエラー

-エンジニア初心者がTypeScriptを学ぶ