この記事について
この記事は私がTypeScriptについて学んだ内容を
TypeScript①(基礎)
TypeScript②(型の機能)
TypeScript③(ユーティリティタイプ)
の3回に分けて網羅的に紹介していきます。
①(基礎)では導入方法と様々な型について簡単な紹介
③(ユーティリティタイプ)では型変換を容易にするユーティリティ型について
今回は型が実際にどういう働きをするのかについてです。
参考リンク
- TypeScript公式ドキュメント
- サバイバルTypeScript 〜実務で使うなら最低限ここだけはおさえておきたいTypeScript入門〜
- TypeScript Deep Dive 日本語版
- 実践TypeScript ~ BFFとNext.js&Nuxt.jsの型定義~ 単行本
型安全
TypeScriptには強力な型推論が備わっており、必ずしも型を定義しないといけないわけではありません。
ですが、場合によって明示的に型を指定して型安全を担保することが必要です。
nullチェック
次の関数では引数がnullの場合でもコンパイルを通してしまい、エラーの発見が遅れる可能性があります。
function foo(num) {
return `${num.toFixed(1)}`;
}
console.log(foo(0.1003)); // 0.1
console.log(foo(null)); // ランタイムエラー(コンパイルは通る)
このようなnumberとnullのどちらの値もありえるという状況はnumber | null
という型で表すことができます。
number | null型の値はnullかもしれないので、numberとして扱ったりプロパティを参照したりすることができません。
function foo(num: numbar | null) {
return `${num.toFixed(1)}`;
}
console.log(foo(0.1003)); // 0.1
console.log(foo(null)); // コンパイルエラー
これに対し、nullでなければ処理したいという場面はif文でnullチェックを行う方法があり、TypeScriptはこれを適切に解釈して型を絞り込んでくれます。
function foo(num: number | null) {
if (num != null) { // この行でのnullの型推論 num: number | null
return `${num.toFixed(1)}`; // この行でのnullの型推論 num: number
} else {
return 0;
}
}
console.log(foo(0.1003)); // 0.1
console.log(foo(null)); // 0
引数をオプションにする
引数に?
をつけることで引数のない場合を許容する事ができます。
しかし以下の場合にはエラーは発生しませんがundefinedが返されます。
function greet(name?) {
console.log(`Hello ${name}`);
}
greet("Mako"); // Hello Mako
greet(); // Hello undefined
上記のようにnameの型は自動的にstring | undefined
になるため、undefinedチェックを行う必要があります。
function greet(name?) {
return name != null ? console.log(`Hello ${name}`) : console.log("Hell0");
}
greet("Mako"); // Hello Mako
greet(); // Hello
readonly
プロパティにreadonly
をつける事でプロパティへの再代入を防ぐ事できます。
type Foo = {
readonly name: string;
};
const foo: Foo = {
name: "Ken",
};
foo.name = "Reo";
console.log(foo); // コンパイルエラー
アノテーション・アサーション
アノテーションは型の注釈のこと
num: number;
アサーションは下記の右辺の型の宣言のこと
let num: number = (<string>comment).length;
let num: number = (comment as string).length;
ダウンキャスト
タイプアサーションを使用して、より具体的なタイプを指定するのがダウンキャストです。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
アップキャスト
ダウンキャストの逆に型の抽象度を上げるのがアップキャストです。
function foo(num: string) {
return console.log((num as any) * 2);
}
foo("3"); // 6
インデックスシグネチャ
下記の[k: string]
をンデックスシグネチャといい、定義されていないプロパティを追加する事ができます。
type Foo = {
[k: string]: any;
};
const foo: Foo = {
name: "Nana",
age: 20,
};
またインデックスシグネチャには様々な制限をかけるができます。
type Fruit = "apple" | "banana" | "lemon";
type Foo = {
[k: string]: Function | Promise<any> | Fruit;
};
const
constで適用される型はWidening Literal Types
という型で、下記のような違いと利用方法があります。
let foo = "foo"; // stringとして推論される
const bar = "bar"; // "bat"と推論される
const Baz = {
a: "a", // a: "a"
b: "b", // b: "b"
c: "c". // c: "c"
} as const
Typeof
typeof演算子は与えられた値の型を文字列で返す演算子です。これを利用して安全に処理することができます。
function foo(value: string | number) {
if (typeof value === "string") {
return console.log(value.length);
}
return console.log(value * 2);
}
foo("100"); // 3
foo(100); // 200
in
in演算子は与えられた値が型に含まれているかの評価結果をbooleanで返します。in演算子を使うことで型の絞り込みが可能です。
type Foo = {
color: string;
point: number;
};
type Bar = {
name: string;
age: number;
};
function log(obj: Foo | Bar) {
if ("color" in obj) { // obj: Foo | Bar
return console.log(obj); // obj: Foo
}
if ("age" in obj) { // obj: Bar
return console.log(obj); // obj: Bar
}
}
このほかにinstanceof演算子を利用した絞り込み方法などもあります。
typeとinterface
型を定義するためにtype、interfaceの二つのの方法がありますが、この二つにはプロパティをオーバーロードできるかどうかという違いがあります。
interface User {
name: string;
}
interface User {
age: number;
}
// Userを利用する際このように結合されます
interface User {
name: string;
age: number;
}
type User = { // User重複によるエラー
name: string
}
type User = { // User重複によるエラー
age: number
}
Generics
型を変数にすることで様々なコンポーネントで再利用できるようにする事ができます。
interface Foo<T> {
foo: T;
}
const bar: Foo<string> = {
foo: "foo",
};
型の初期値を指定
interface Foo<T = string> {
foo: T;
}
extendsによる型の制約
function foo<T extends string>(value: T) {
console.log(value);
}
foo("Hello"); // Hello
keyof と T[K]
keyof T
でT
の型が付与され、T[K]
でT
の型が持つK
の型が付与される。
これによりキャストの省略が可能になる。
interface Foo {
bar: string;
baz: number;
}
type Hoge = keyof Foo; // string | numbar
type Fuga = Foo["bar"]; // string
type Peyo = Foo["baz"]; // number
keyofとT[K]を組み合わせた利用。
function hoge<T, K extends keyof T>(user: T, key: K): T[K] {
return user[key];
}
interface User {
name: string;
age: number;
}
const user = {
name: "Takeru",
age: 25,
};
const foo = hoge(user, "name"); // const foo: string
const bar = hoge(user, "age"); // const bar: number
Mapped Types
Mapped Typesは{[P in K]: T}
という構文を持つ型です。
これが何をするかというと雑にいえば型世界でのmapメソッドです。
{[P in K]: T} // Pの中身をK、その型をTにする
type Item = { a: string; b: number; c: boolean; };
type Foo = {[P in keyof Item]: string}
// Pの中身を{a, b, c}にして、その型をstringにする{a: strting, b: string, c: string}
// それをFooに代入している
// type Foo = {a: strting, b: string, c: string}
Conditional Types
Conditional TypesはT extends U ? X : Y
という構文を持つ型です。
三項演算子を利用して条件分岐ができます。
type Foo<T> = T extends string ? true : false; // string型をture型に変換し、それ以外の型をfalse型に変換します。
type Bar = Foo<"foo">; // type Bar = true
type Baz = Foo<0>; // type Baz = false
Mapped Types × Conditional Types
{[P in K]: T}
とT extends U ? X : Y
を組み合わせた例です。
type Item = { a: string; b: number; c: boolean };
type Foo<T, U> = {
[K in keyof T]: T[K] extends U ? true : false;
};
type Bar = Foo<Item, string>; // type Bar = {a: true; b: false; c: false; };
type Baz = Foo<Item, number>; // type Baz = { a: false; b: true; c: false; };
今回は型で型を変換するという複雑な内容でしたが、次のTypeScript③(ユーティリティタイプ)ではスクラッチで型変換ロジックを作成なくても、TypeScriptが元々持っている型変換を容易にできる型(ユーティリティタイプ)のついて紹介します。