まえがき
「JavaScriptじゃなくてTypeScriptでしょ!』という世の中になったくらいでエンジニアになったため、JavaScriptよりTypeScriptの方を書いてきた人生であった。しかし、いまだに下記のことを自信をもって説明できないのが現状なので、本稿にまとめていく。
- なんでTypeScript使うの?
- 型定義ファイル(d.ts) と declare宣言
- typeとinterface
- ScriptとModule
- グローバルオブジェクトとグローバル変数
- 名前空間とnamespace宣言
- モジュール
なぜTypeScriptを使うのか?
JavaScriptに任意の型システムを追加することで、コードの品質と読みやすさが高めるため。
・開発中に早い段階でエラーに気づける。(JSだと実行時にエラーが出るが、TSだとコンパイル時にエラーが出る)
今まで書いてきたJavaScriptをどうやってTypeScriptに移行する?
TypeScriptを使った方がいいのはOK。では、今まで作られてきたJavaScriptライブラリをTypeScriptで扱う場合、どうやって型情報を割り当てるのか?ここで「型定義ファイル(d.ts)とdeclare宣言」を利用する。
型定義ファイル(d.ts) と declare宣言
declare宣言 で、既存のJavaScriptに型情報を追加する。
既存のJavaScriptで下記のようなコードがある場合、TypeScript(--.ts)として扱うと変数$が定義されていません
とエラーになる。
$('.awesome').show(); // エラー: `$` が存在しません。
簡単な修正方法は、declare宣言を使って、グローバル変数$が、実行時に必ず存在することをTypeScriptに伝えることである。
declare var $: any; // TypeScript型推論器に「変数$の型はany」と伝える。
$('.awesome').show(); // 変数$の型はanyだから、問題なしと判断される。
declare宣言は型定義ファイル(.d.ts)に定義する。この型定義ファイルがあれば、既存JavaScriptをTypeScriptで扱うことが可能となる。
型定義ファイル(.d.ts)とは
・declare宣言、複数ファイルで利用したい型(type/interface)を記述するファイル。
・型定義ファイルに定義されたdeclare宣言/型情報(type/interface)は、TSコンパイル時に型推論器に渡される。
・型定義ファイル(.d.ts)に対して、JSファイルは生成されない。(型情報が必要なのはTSコンパイルする時だけだもんね)
・有名なJavaScriptライブラリの型定義ファイルの作成は、DefinitelyTypedコミュニティにて行われており、npm i --save-dev @types/ライブラリ名
でダウンロードできる。つまり、
- JavaScriptライブラリをTypeScriptで利用したい。
↓ - DefinitelyTypedコミュニティが作成してたらその型定義ファイルを使う。
- DefinitelyTypedコミュニティが作成してなかったら、自分で型定義ファイルを作成して、TSコンパイルを通るようにする。
という流れになる。
アンビエント(ambient) = 実装を定義しない宣言
TypeScriptにおけるdeclare宣言はアンビエント(ambient)宣言
とも呼ばれる。
この"アンビエント(ambient)"とは、実装を定義しない宣言のことを指している。
We call declarations that don’t define an implementation “ambient”. Typically, these are defined in .d.ts files.
型定義ファイル、自分で書いてみる。
説明読んだだけだと自分で使える気が全然しないので実際に使ってみる。
@typesディレクトリ配下にindex.d.ts
を作成
type Person = {
name: string;
age: number;
}
type T = number;
tsconfig.json
のincludes
に@types/index.d.ts
を追記。
「includes
= コンパイル対象とするもの」なので、型定義ファイルをコンパイル対象に入れることで、@types/index.d.ts
に定義した型を他のファイルから参照できるようになる。
{
"include": ["src", "@types/index.d.ts"]
}
型が利用できる事の確認
index.d.ts
に定義したPerson
とT
を以下のように利用できる。
const person: Person = {
name: "Taro",
age: 23
};
const count: T = 3;
type と interface の違い
type
・正式名称「Type Alias」
・「Alias」なので、あくまで「既存の型を参照(alias)して新たな型を定義するもの」である。
↓の例が一番わかりやすい。
type T = number;
const count: T = 3;
・↑のように、変数にオブジェクトを代入することで定義できる。
interface
・class, functionと同じように定義できる。
・拡張に対してオープンであり、後付けでプロパティを追加することができる。
interface Point {
x: number;
y: number;
}
const p: Point = { x: 10, y: 20 };
// Point インタフェースにプロパティを追加
interface Point {
z: number;
}
const p: Point = { x: 1, y: 2, z: 3 };
typeとinterfaceの共通点/相違点
共通点
・オブジェクトの型として扱える。
・プロパティもメソッドも定義できる。
相違点
・implementsしてclass定義できるのはinterface
のみ。
typeとinterface いつどちらを使うべき?
似たような使い方ができちゃうから混乱したが、よく考えると用途が異なる。
type
:既存の型を用いてオブジェクトの型を定義したい時に使う。
interface
:あるモジュール/まとまった処理に対してIFだけ定義したい場合に使う。
実装は継承クラスに任せる。(Javaの使い方と同じイメージ)
ScriptとModule
ECMAScriptではプログラムはScriptとModuleの2種類に分類されている。
プログラム種別 | 特徴 |
---|---|
Script | ・import/exportの記載がないもの ・<script>~~~</script> |
Module | importもしくはexportの記載があるもの ・<script type="module">~~~</script> |
逆に、import文/export文はModuleの中でしか使えない構文である。
Scriptの場合
// fuga.ts
const msg = "HELLO WOLRD";
console.log(msg);
<!-- index.html -->
<script>
'use strict';
var foo = 123;
console.log(window.foo);
</script>
Moduleの場合
// fuga.ts
import hoge from "./hoge";
hoge.hello();
<!-- index.html -->
<script type="module">
'use strict';
var foo = 123;
console.log(globalThis.foo); // undefined Moduleではvar変数はグローバル変数とならない
</script>
グローバル変数
文字通り、グローバルに(どこからでも)参照できる変数のことである。
グローバル変数はグローバルオブジェクトに格納される。
グローバルオブジェクト
実行環境 | グローバルオブジェクト名 |
---|---|
ブラウザJavaScript | window か globalThis |
NodeJS | global か globalThis |
両環境で利用される可能性があるコンテキストの場合はglobalThis
を使った方が良さそう。
グローバル変数の定義方法
プログラム種別(Script/Module)で、グローバル変数の定義の仕方が変わる。
プログラム種別 | グローバル変数の定義方法 |
---|---|
Script | グローバルスコープでvar変数/function等を定義 |
Module |
型定義ファイル(.d.ts)以外の場所でdeclare global 宣言例) ReactだったらルートのApp.tsxに定義すればOK |
Scriptでグローバル変数を追加する場合
var counter = 0;
console.log(window.counter);
Moduleでグローバル変数を追加する場合
declare global {
var counter: number;
}
export function freshId() {
window.counter ??= 0;
return counter++;
}
名前空間 と namespace宣言
「名前空間」はJavaScript/ECMAScriptの概念。
「namespace宣言」はTypeScript固有の文法。
「namespace宣言」で「名前空間」を作成する という関係性である。
名前空間とは
・グローバルスコープに定義したvar変数に「複数の関連する値がまとめられたオブジェクト」を代入する。このvar変数が「名前空間」である。
・グローバルスコープに定義したvar変数は、windowオブジェクトのプロパティとなるため、どこからでも参照できるもの、すなわち一意なグローバル変数となる。
// グローバルスコープにてvar変数を定義
var MYFUNCTIONS = {
addition: function(num1,num2){
return num1+num2;
},
multiplication: function(num1,num2){
return num1*num2;
}
}
// 名前空間(windowオブジェクトの変数)である MYFUNCTIONS を参照
var operation = MYFUNCTIONS.addition(5,10);
console.log(operation)
名前空間 = 複数の関連する値がまとめられたオブジェクト
であることを踏まえると、
モジュールも名前空間である。
import * as hoge from "./hoge";
// hogeモジュールがexportしているものが、hogeオブジェクト(名前空間)内にはいっている。
hoge.hello();
hoge.world();
※ import * as ns
は名前空間IMPORTとも呼ばれる。ns.XXXX で参照できるから。
namespace宣言とは
・JavaScriptではグローバルスコープでvar変数を定義する
ことで名前空間を作成したが、TypeScriptではTypeScript特有のnamespace宣言を利用して、名前空間(複数の関連する値がまとめられたオブジェクト)を作成できる。
// module Foo でもよい。moduleもnamespaceも結局「複数の関連する値がまとめられたオブジェクト」を生成するのだから。
namespace Foo {
export const x = 42;
export type T = number;
}
// 名前空間からのインポートには専用構文がある
import V = Foo.T;
モジュールとは?
モジュールとは
・export キーワードを含む .ts ファイル
・モジュールは名前空間(複数の関連する値がまとめられたオブジェクト)である。
・declare module
で定義したもの
モジュールIMPORT/EXPORT方法
export interface MyInterface {
name: string;
}
export class MyClass implements MyInterface {
constructor(public name: string) {}
}
をIMPORTする。
個別にIMPORT
import { MyInterface, MyClass } from './lib/mylib';
const obj: MyInterface = new MyClass('Maku');
console.log(obj.name); //=> Maku
まとめてIMPORT
import * as mylib from './lib/mylib';
const obj: mylib.MyInterface = new mylib.MyClass('Maku');
console.log(obj.name); //=> Maku
ここでは、as キーワード を使って mylib という変数のプロパティ経由で公開されているインタフェースにアクセスできるようにしています。 ワイルドカードを使用する場合、この変数の指定は必須 であることに注意してください(それにより、不注意による名前の衝突を防げるようになっています)。
declare moduleを使ってモジュールを作成する場合
// export module
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
export function parse(
urlStr: string,
parseQueryString?,
slashesDenoteHost?
): Url;
}
// import module
import * as URL from "url";
let myUrl = URL.parse("https://www.typescriptlang.org");