TypeScript触った事ないけど、型についてまとめてみた。
最初に
今の所 JavaScript → React → Next.js と来ているので TypeScript もやろうと思う。(JavaScript 以外はまだ考えてアプリを作成できるレベルじゃない)
まだ良さが分からない TypeScript を勉強して大好きになると思う。
巷では型を定義出来るのが良いらしい。それで型についてまとめてみた。
環境構築
個人的に -g
を付けるグローバルインストールは環境を汚染する気がして苦手なのでローカルインストールを行います。
そのため tsc
コマンドのみでは実行出来ないため、直接パスを指定する方法か npx tsc
とする方法の 2 通りを推奨しています。
- Node.js インストール
- 作業ディレクトリを作成
npm install typescript --save-dev
- 上記のコマンドはローカルインストールなので、tsc コマンドを実行する場合はパス指定する必要がある。
../node_modules/.bin/tsc -v
- tsconfig.json の作成
../node_modules/.bin/tsc --init
コンパイル時の設定を変更できる。ない場合はデフォルトの設定が使用される。 - 実行は
../node_modules/.bin/tsc コンパイルしたい.ts
コンパイルして -
node index.js
で実行する。 -
npm install --save-dev @types/node
すると node.js のパッケージが使用できる。fs
等 - パスの指定が毎回面倒な場合は
npx
コマンドを用いることでパスを省略出来る。npx tsc -v
等
アンビエント宣言(declare)
型定義と呼ばれるもので declare var x: number
等の記述を 型定義ファイル(.d.ts)
と呼ばれる別ファイルで管理する。
下記のコードは TypeScript では変数の宣言がされてないとしてエラーになる。
x = 30; // error TS2304: Cannot find name 'x'.
console.log(x); // error TS2304: Cannot find name 'x'.
なので
型定義ファイルか同じファイルに declare var x: number;
と追加する事でエラー解決する事ができる。
基本的には別で作成した型定義ファイルに記述する事が推奨されている。
そして、JavaScript のライブラリ等では型定義ファイルが用意されている事が多いのでそちらを使う。この型定義があると IDE 等でのコード補完機能が得られる。
型注釈
変数の隣に :
と書いてその右側に型の注釈を入れる。
let hasValue: boolean = true;
型が推論出来ない時に使用する。
例:初期値の入力で変数を代入しない時等。
let hasValue: boolean;
型推論
TypeScript がどの型か変数の値から自動で教えてくれる。
オブジェクトの型書き方
const a: object = {
name: "daigakuseidatta",
age: 25,
};
//a.name と実行してもそんなのないと言われる
//オブジェクトリテラル型で型定義する。
const person: {
name: string;
age: number;
} = {
name: "Jack",
age: 21,
};
//オプショナルプロパティを使う(?)
let person: {
age: number;
lastName: string;
readonly first: string;
gender?: string; //?で会ってもなくてもよくなる。
} = {
age: 28,
latsName: "sato",
firstName: "tiaki",
//genderがなくても怒られない。
};
person.gender = "male"; //後から追加できる。
person.lastName = "kobayashi"; //上書きできる。
person.firstName = "kuniko"; //上書き出来ない。
//インデックスシグネチャ
// オブジェクトが複数のプロパティを持つ可能性を示す。
// [key:T]: Uのように定義する。
// keyはstringかnumberのみ
const capitals: {
[countryName: string]: string;
} = {
Japan: "Tokyo",
Korea: "Seoul",
};
capitals.China = "Beijing";
capitals.Canada = "Ottawa";
//ネストする場合
const person: {
name: {
first: string;
last: string;
};
age: number;
} = {
name: {
first: "Jack",
last: "Smith",
},
age: 21,
};
//オブジェクト内に関数がある場合
const Falcon: {
name: string;
fly: () => void;
} = {
name: "鷹",
fly: function (): void {
console.log(this.name + "が大空を飛びました");
},
};
Falcon.fly(); // '鷹が大空を飛びました
配列の型書き方
const fruits: string[] = ["Apple", "Banna", "Grrape"];
列挙型
特定のまとまった型を受け入れる。
enum CoffeeSize {
SHORT = "SHORT",
TALL = "TALL",
GRANDE = "GRANDE",
VENTI = "VENTI",
}
const coffee = {
hot: true,
//size: 'SHORT'この文字しか入らないようしたい。
//こうすると (prorerty) size: CoffeeSizeという列挙型になる。
size: CoffeeSize.SHORT,
};
//入らない
coffee.size = true;
coffee.size = "SHOT";
//入る
coffee.size = CoffeeSize.SHORT;
union 型(合併型)・Intersection 型(交差型)
合併型
複数の型を指定したい時に使用する。
型 A か型 B のどちらかの型を持つ
交差型
型 A と型 B 両方の型を持つ
交差型は「A と B に共通する型」ではない。
//合併型を簡素に使用した場合
let unionType: number | string = 10;
type Knight = {
hp: number;
sp: number;
weapon: string;
swordSkill: string;
};
type Wizard = {
hp: number;
mp: number;
weapon: sting;
magicSkill: string;
};
// 合併型...KnightまたはWizardの型を持つ。
type Adventurer = Knight | Wizard;
// 交差型... KnightかつWizardの型を持つ
type Paladin = Knight & Wizard;
//合併型の場合ここにmpが追加されてもエラーにならない。
//指定された型があってもなくても良い。
const adventure1: Adventure = {
hp: 100,
sp: 30,
//mp: 30,
weapon: "木の剣",
swordSkill: "三連斬り",
};
//Wizard寄りの冒険者
const adventure2: Adventurer = {
hp: 100,
mp: 30,
weapon: "木の枝",
magicSkill: "ファイアボール",
};
//交差型は合併したものが全て揃ってないとだめ。
const paladin: Paladin = {
hp: 300,
sp: 100,
mp: 100, //一つでも型が欠けるとエラーになる。
weapon: "銀の剣",
swordSkill: "日輪",
magicSkill: "アルティメットキャノン",
};
リテラル型
それしか代入出来なくなる。
//エラーになる。
const apple: "apple" = "hello";
//エラーなし。
const apple: "apple" = "apple";
uinon 型 + リテラル型
let cloathSize: 'small' | 'medium' | 'large' = 'large';
const cloth = {
color: 'white',
size: clothSize
}
//上記の状態ではsizeはリテラル型になって'large'しか受け付けない。
cloth.size = 'small'
//そこでオブジェクトの型定義でユニオンを使用する。
const cloth = {
color: 'white';
//この3つしか入らないくなる。
size: 'small' | 'medium' | 'large'
} = {
color: 'white',
size: 'medium'
}
型エイリアス
- type を使って、型に名前を付けて宣言できる。
- 同じ型を何度も定義する必要がない。(再利用性が高い)
- 型に名前を付けることで変数の役割を明確化できる。
先ほどの'small' | 'medium' | 'large'
を変数みたいに格納したい。
//独自の型を定義した感じになる。
type ClothSize = 'small' | 'medium' | 'large'
const cloth = {
color: 'white';
//この3つしか入らないくなる。
size: ClothSize
} = {
color: 'white',
size: 'medium'
}
type Country = {
capital: string
language: string
name: string
}
const japan: Country = {
capital: 'Tokyo',
language: 'Japanese',
name: 'Japan'
}
Interface・Type Alias 使い方
2021 年時点でマイクロソフトは Interface の方が拡張しやすいという。理由から推奨している。
Interface
- interface 宣言子で定義する。
- Type Alias と違って「=」は不要(一種のクラスなので=がいらない)
interface Bread {
calories: number;
}
- 同名の interface を宣言すると型が追加(マージ)される後から型を追加定義できる。
- 宣言のマージ:同じ名前を共有する複数の宣言を自動的に結合
interface Bread {
type: string;
}
const francePan: Bread = {
calories: 350,
type: "hard",
};
実際に色々、継承してみる。
interface Bread {
calories: number;
}
interface Bread {
type: string;
}
const francePan: Bread = {
calories: 350,
type: "hard",
};
//上記のinterfaceを型エイリアスで表現
type MaboDofu = {
calories: number;
spicyLevel: number;
};
type Rice = {
calories: number;
gram: number;
};
type MaboDon = MaboDon & Rice; // 交差型(Intersection)
const maboDon: MaboDon = {
calories: 500,
spicyLevel: 10,
gram: 350,
};
Interface の拡張
- extends を使う事で継承したサブインターフェースを作れる。
- Type Alias を extends することも出来る。
interface Book {
page: number;
title: string;
}
interface Magazine extends Book {
cycle: "daily" | "weekly" | "monthly" | "yearly";
}
const jump: Magazine = {
cycle: "weekly",
oage: 300,
title: "週刊少年ジャンプ",
};
//type Aliasを継承してみる。
type BookType = {
page: number;
title: string;
};
interface Handbook extends BookType {
theme: string;
}
//最後はInterfaceの型で変数を作成する。
const nijiiro: HandBook = {
page: 138,
title: "にじいろ",
theme: "旅行",
};
Interface で class に型を定義する。
- implements(実装する)を使って class に型を定義する。
interface Book {
page: number;
title: string;
}
//Bookを実装するComicと読めば良い。
//Bookを実装するComicは必ずpage、titleというプロパティを持つ
class Comic implements Book {
page: number;
title: string;
constructor(page: number, title: string, private publishYear: string) {
this.page = page;
this.title = title;
}
getPublishYear() {
return this.title + "が発売されたのは" + this.publishYear + "年です。";
}
}
const popularComic = new Comic(200, "鬼滅の刃", 2016);
Type Alias と Interface の違い
Type Alias
用途:複数の場所で再利用する型に名前を付ける。
拡張性:同名の type を宣言するとエラー
継承:継承はできない交差型で新しい型エイリアスを作る
使用できる型:オブジェクト関数以外のプリミティブ、配列、タプルも宣言可能
考慮事項:拡張しにくい不便さがある。
いつ使う:アプリ開発では Type Alias
Interface
用途:オブジェクト・クラス・関数の構造を定義するため
拡張性:同名の interface を宣言するとマージされる(宣言のマージ)
継承:extends による継承ができる
使用できる型:オブジェクトと関数の型のみ宣言できる
考慮事項:拡張できることによりバグを生む可能性
いつ使う:ライブラリ開発では Interface
関数に型を付ける
引数と戻り値に型を付ける。
function add(num1: number, nunm2: number): number {
return num1 + num2;
}
Void 型
関数が何も返さない(undefind が一応返ってくる)物だとどうなるのか。
TypeScript には undefined
型は存在するが関数の型定義で使用するとエラーになるので登場するのは稀になる。ただし関数の最後に return
が存在する場合は undefined
型を使用する事ができる。
そこで関数で undefined
が返ってくる場合は型定義で void
を使用する。
function sayHello(): void {
console.log("Hello");
}
// 実行するとundefindを一応返す。
// なのでundefined型も存在する。
let tmp: undefined;
Null 型
let tmpNull: null = null; //ok
let tmpNull: null = undefined; //ok
//この逆でundefinedにnullをいれても良い。
無名関数に型書き方
const anotherAdd: (n1: numebr, n2: number) => number = function (
num1: number,
num2: number
): number {
return num1 + num2;
};
//片方の型を省略できる。
const anotherAdd: (n1: numebr, n2: number) => number = function (
num1,
num2
): number {
return num1 + num2;
};
アロー関数の型書き方
const doubleNumber = (num: number): num => num * 2;
//こんな感じに型定義をずらす事もできる。 //戻り値の型
const doubleNumber2: (num: number) => number = (num) => num * 2;
コールバック関数の型書き方
関数自体は戻り値を持たない。第一引数は number を型に持つ、第二引数は関数でその関数の引数は number を型に持ち戻り値も number を返す。
function doubleAndHandle(num: number, cb: (num: number) => number): void {
const doubleNum = cb(num * 2);
console.log(num * 2);
}
// コールバック関数の引数にはdoubleAndHandleの第一引数が入る。
doubleAndHandle(21, (doubleNum) => {
return doubleNum;
});
//仮にコールバック関数の戻り値をvoidの型にするとエラーは起きないが戻り値は無効となり使用出来なくなる。
function doubleAndHandle(num: number, cb: (num: number) => number): void {
const doubleNum = cb(num * 2);
//if(doubleNum) //doubleNum.toString()とかにするとエラーになる。
console.log(doubleNum);
}
doubleAndHandle("21", (doubleNum) => {
return doubleNum;
});
Generics(ジェネリックス)
ジェネリックスとは抽象的な型引数を使用して、実際に利用されるまで型が確定しないクラス・関数・インターファイスを実現する為に使用される。
通常の型定義
// number型 関数の引数・戻り値は数値と型定義している
function test(arg: number): number {
return arg;
}
// 上記の文字列版
function test2(arg: string): string {
return arg;
}
test(1);
test2("hello");
ジェネリックを使用した型定義
// ここで型を定義しないで関数を使用する際に型定義する
function test<T>(arg: T): T {
return arg;
}
test<number>(1);
test<string>("hello");
Any 型・Unknow 型
- any 型はなんでも入る型、便利だが TypeScript の機能を放棄しているようにも思える。
- unknon 型は any 型より少し厳しい型
let unknownInput: unknown;
let anyInput: any;
let text: string;
unknownInput = "hello";
unknownInput = 21;
unknownInput = true;
text = anyInput; //エラーにならない。使うときもエラーにならない。
text = unknownInput; //ここでエラーになる。何でも入れられるが使うときに注意がでる。
//unknownの使い方 stringの型が来たときのみ代入する。
if (typeof unknownInput === "string") {
text = unknownInput;
}
Never 型
エラーをキャッチする関数等の戻り値として使用される。
他にはずっとループで周り続ける関数の戻り値に使用されることもある。
// もしneverが書かれていない場合、TypeScriptの型推論ではvoid型になる。
// 理由はnever自体が新しい型でバージョンから登場してまだ追いついていない。
function error(message: string): never {
throw new Error(message);
}
console.log(error("This is an error"));
最後に
調べる前は String, Number 等の引数型があらかじめ分かっても、そこまで読みやすくなるのかと思っていた。オブジェクトの型、配列の型で細かく型を設定できると知り確かにこれなら戻ってくる Json ファイルの型とかを書いておけば後でコードを読んでも実際に実行する機会とかを減らせして効率が上がるのかなと思った。まだ途中なので色々と勉強していきたいと思います。
記事に関するコメント等は
🕊:Twitter
📺:Youtube
📸:Instagram
👨🏻💻:Github
😥:Stackoverflow
でも受け付けています。どこかにはいます。
参照
TypeScript |アンビエント宣言(declare)と型定義ファイル(.d.ts) - わくわく Bank
超 TypeScript 入門完全パック- TypeScript でアプリを作りたい方必見!
【日本一わかりやすい TypeScript 入門】型エイリアス(type)でオブジェクトの型定義
【日本一わかりやすい TypeScript 入門】ハンズオンで理解する Interface と Type Alias の違い
Discussion