BitArts Blog

ロードバイク通勤のRubyプログラマで伊豆ダイバー。の個人的なブログ。

TypeScript基礎

少し勉強して社内勉強会で発表したので置いておきます。

TypeScript基礎


参考文献

プロを目指す人のための
TypeScript入門 安全なコードの書き方から高度な型の使い方まで

技術評論社

  • 鈴木僚太 著
  • B5変形判/424ページ
  • 定価3,278円(本体2,980円+税10%)
  • ISBN 978-4-297-12747-3

TypeScriptとは

  • AltJS(JavaScriptに変換して利用する言語=トランスパイラ)
    • かつてはCoffeeScriptとか色々あったが、今はTypeScript一強
  • JavaScriptに型チェックの機能だけを追加したもの
    • 初期のころに実装された独自機能が少しあるが、今は型定義の機能に限定して設計されている
  • JavaScriptの型チェッカーとしてはかつてはFlow (Facebook)などがあったが、今はTypeScript一強
  • DenoやBunではトランスパイル不要で.tsのまま実行できる
  • ブラウザでは型周りを無視して実行するという提案なども出ていてWeb標準化が進みつつある

TypeScriptを学ぶ前に

  • ES6以降のJavaScript
    • 同時に学ぼうとするとかえって効率が悪い。先にES6を学んでおこう

TypeScriptのメリット・デメリット

メリット

  • 多くのエラーを実行前に検出できる
  • 型が仕様書として機能する
  • エディタの開発支援機能の恩恵

デメリット

  • コンパイルが必要
    • 開発環境ではソース保存時に瞬でコンパイルされるので、ほぼ意識することはないが
  • コード量が多くなる

ゆるくTypeScriptへ移行するには

  • 全部型定義しなければならないわけではない
  • 利用するライブラリが型付きになるだけでもメリットあるから無理せず
  • ライブラリなどの型定義を見る機会が増えるので、構文はできるだけ知っておいたほうがよさそう
  • まずはとにかく拡張子を .js -> .ts に
  • デフォルトでは関数の引数の型注釈だけは必要
    • オプションで不要にできる(noImplicitAny)
  • 多くは型を明示しなくても型推論が働く
  • 型注釈が必要なところは、まずは any 型や空のオブジェクト型 {} 指定でユルく
  • 多く使われる箇所から徐々に型を定義していく
  • 機能が多く複雑な印象があるが、柔軟な型定義のためのものなので全部使う必要はない

TypeScript開発環境を作る

少し昔はJavaScriptは開発環境作るの大変だったけど、今は超簡単になっている

viteおすすめ。npm入っていれば一瞬で環境作れる

$ npm init vite
? Project name: › hello-vite-ts # ①プロジェクト名を入力
? Select a framework: › - Use arrow-keys. Return to submit.
❯   vanilla # ②フレームワークを選択(vanilla = フレームワークを使わない)
    vue
    react
    preact
    lit-element
    svelte
? Select a variant: › - Use arrow-keys. Return to submit.
    vanilla
❯   vanilla-ts # ③TypeScriptを使用する
$ cd hello-vite-ts
$ npm install
$ npm run dev

型注釈の基本

変数に型注釈を与える

普通のJavaScriptでの変数宣言

const greeting = "Hello!";

TypeScriptでは次の構文で型を指定できる

const 変数: 型 =;

const greeting: string = "Hello!";

const greeting: string = 123; //=> 型 'number' を型 'string' に割り当てることはできません。ts(2322)

string型で宣言した変数に数値は代入できない(コンパイルエラー)


型推論

型注釈を書かなくても 型推論により常に型は存在する

let greeting = "Hello!";

greeting = 123; //=> 型 'number' を型 'string' に割り当てることはできません。ts(2322)

JavaScriptでは動くコードだが、TypeScriptではコンパイルエラーになる


プリミティブ型

基本の組み込み型

  • string
  • number
  • boolean
  • bigint
  • null
  • undefined
  • symbol

※ symbolはあまり使わない


オブジェクト型

普通にオブジェクトを作った場合

const obj = {
  foo: 123,
  bar: "Hello!"
};

型推論により次にように型定義される

const obj: {
  foo: number,
  bar: string
};

明示的に定義するにはこうする

const obj: {
  foo: number,
  bar: string
} = {
  foo: 123,
  bar: "Hello!"
};

あるいは、type文で新しい型を宣言する(型情報を再利用できる)

type FooBar = {
  foo: number;
  bar: string;
};

const obj: FooBar = {
  foo: 123,
  bar: "Hello!"
};

似た記法でinterface宣言を使う方法もあるが、ほぼ同じなので割愛


インデックスシグネチャ

どんな名前のプロパティでも受け入れる

type PriceList = {
  [key: string]: number;
};

const data: PriceList = {};

data.orange = 123;
data.apple = "Hello!"; //=> 型 'string' を型 'number' に割り当てることはできません。ts(2322)

好きな名前のプロパティを作れる = 型安全性を破壊する場合があるので注意


オプショナルなプロパティ

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

const obj1: FooBar = { foo: 123 };
const obj2: FooBar = { foo: 123, bar: "Hello!" };

console.log(obj1.bar); //=> "undefined"

読み取り専用プロパティ

type Foo = {
  readonly foo: number;
};

const obj: Foo = { foo: 123 };

obj.foo = 456; // => 読み取り専用プロパティであるため、'foo' に代入することはできません。ts(2540)

部分型関係

部分型とは

他の型と同じ構造を含んでいるなら、その型でもある

type Foo = {
  foo: number;
};

type FooBar = {
  foo: number;
  bar: number;
};

const obj1: FooBar = { foo: 123, bar: 456 };
const obj2: Foo = obj1;

console.log(obj1.bar); //=> "456"
console.log(obj2.bar); //=> プロパティ 'bar' は型 'Foo' に存在しません。ts(2339)

TypeScriptにおいて非常に重要な概念


配列の型

number型の値を要素に持つ配列は次のように定義できる

const arr: number[] = [1, 2, 3];

number以外は入れられない

const arr: number[] = ["Hello!"]; //=> 型 'string' を型 'number' に割り当てることはできません。ts(2322)

次のように書くこともできる(Arrayは組み込みのジェネリック型 ※後述)

const arr: Array<number> = [1, 2, 3];

読み取り専用配列型

const arr: readonly number[] = [1, 2, 3];

arr[0] = 10; //=> 型 'readonly number[]' のインデックス シグネチャは、読み取りのみを許可します。ts(2542)

次のように書くこともできる

const arr: ReadonlyArray<number> = [1, 2, 3];

配列型とユニオン型

複数の型の要素を受け入れるには、ユニオン型を使う

const arr: (number | string)[] = [123, "Hello!"];

タプル型

タプル=要素数が固定された配列のこと

それぞれの要素に型を指定できる

const tpl: [number, string] = [123, "Hello!"];

関数の型

JavaScriptでは関数も変数に入るので、他の変数と同じように型を定義できる

type SayHelloFunc = (name: string) => string;

const sayHello: SayHelloFunc = (name) => {
  return `Hello, ${name}.`;
};

型情報に本来必要のない引数名(name)まで含まれている
エディタのコーディング支援機能のためであり、関数の型情報がドキュメントとして機能する


型推論されるのでtype宣言は省略してもいい

const sayHello = (name: string): string => {
  return `Hello, ${name}.`;
};

型引数とジェネリック

型を利用するときにパラメータを指定できる

type FooBar<T1, T2> = {
  foo: T1;
  bar: T2;
};

const obj1: FooBar<string, number> = { foo: "Hello!", bar: 123 };

const obj2: FooBar = {}; //=> ジェネリック型 'FooBar' には 1 個の型引数が必要です。ts(2314)

型引数を持つ型のことを「ジェネリック型」と呼ぶ

動的に型を作る機能。似た構造の型をいろいろ扱いたい時に有用


オプショナルな型引数

型引数のデフォルト値を指定できる

type FooBar<T1 = string, T2 = number> = {
  foo: T1;
  bar: T2;
};

const obj: FooBar = { foo: "Hello!", bar: 123 };

基本は型が決まっているが、たまに変えたいときに有用


ジェネリクス

型引数を持つ関数(ジェネリック関数)

const repeat = <T>(element: T, length: number): T[] => {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
};

console.log(repeat<string>("a", 5)); //=> ["a","a","a","a","a"]
console.log(repeat<number>(123, 3)); //=> [123,123,123]

// 型推論されるので型引数は省略してもok
console.log(repeat("a", 5));

戻り値にもT型が使われている

stringが入力されたらstring[]を返したいなど、引数に応じて戻り値の型が決まるようなケースで利用される


インターセクション型

「T & U」のように書き、「T型でありかつU型でもある値」を意味する型

type Animal = {
  species: string;
  age: number;
}

type Human = Animal & {
  name: string;
}

このようにオブジェクト型を拡張して新しい型を作る用途で使われることが多い


lookup型

型名を直接書くのではなく「Human型のageプロパティの型」というように指定できる

type Human = {
  age: number;
};

const setAge = (age: Human["age"]) => {
  // ...
};

// これと同じ意味
const setAge = (age: number) => {
  // ...
};

将来ageの型がbigintに変わったりしても安心

ただし一見して何の型だか分からなくなるため用途に注意
特定のプロパティの値を渡してほしい、という意思表示に使う