基本的なtsconfig.jsonの設定を理解されている前提で話が進みますのでまだの方はすでにある本書のtsconfig.jsonのページをご覧ください。
パッケージを公開する時は、動作する形で公開するのが前提なのでjs
にする必要があります。つまりトランスパイルは必須です。ですがせっかくTypeScriptで作ったのだからパッケージの型情報も提供しましょう。
型定義ファイルを一緒に出力しましょう。そのためにはtsconfig.jsonにあるdeclaration
の項目をtrue
に変更します。
"declaration": true,/* Generates corresponding '.d.ts' file. */
このように設定するとトランスパイルで出力したjs
ファイルと同じディレクトリに同名で拡張子がd.ts
のファイルも出力されるようになります。これが型情報のファイルです。なおこの型定義ファイルだけをトランスパイルで出力されたjs
と別のディレクトリに出力するためのオプションは存在しません。
変哲もないnumber
を持つValue Object
を作ったとします。
class NumericalValueObject {private value: number;public constructor(value: number) {this.value = value;}public eq(other: NumericalValueObject): boolean {return this.value === other.value;}public gt(other: NumericalValueObject): boolean {return this.value > other.value;}public gte(other: NumericalValueObject): boolean {return this.value >= other.value;}public lt(other: NumericalValueObject): boolean {return this.value < other.value;}public lte(other: NumericalValueObject): boolean {return this.value <= other.value;}public toString(): string {return `${this.value}`;}}
これをトランスパイルし、型定義を生成するとこのようになっています。
declare class NumericalValueObject {private value;constructor(value: number);eq(other: NumericalValueObject): boolean;gt(other: NumericalValueObject): boolean;gte(other: NumericalValueObject): boolean;lt(other: NumericalValueObject): boolean;lte(other: NumericalValueObject): boolean;toString(): string;}
内容自体はちょうどインターフェースのようなファイルとなっています。
IDEを使っているときに有用で、実際のts
のソースコードがどのようにして動作しているのかを閲覧することができるようになります。tsconfig.jsonにあるdeclarationMap
の項目をtrue
に変更します。
"declarationMap": true,/* Generates a sourcemap for each corresponding '.d.ts' file. */
このように設定するとトランスパイルで出力したjs
ファイルと同じディレクトリに同名で拡張子がd.ts.map
のファイルも出力されるようになります。このファイルは元のts
と実際に動作するjs
の対応付けをしてくれます。ただしこの設定だけでは不十分で、参照元となる元のts
ファイルも一緒にパッケージとして公開する必要があります。
特に設定していなければ元のts
ファイルも公開されますが、公開する内容を調整している場合は逆にpackage.jsonのfiles
プロパティをを変更して元のts
ファイルも公開するように変更が必要です。tsconfig.jsonのdeclarationMap
を設定しても元のts
ファイルを参照できないときはここで公開する内容を制限していないか確認してください。
{"name": "YYTS","version": "1.0.0","license": "CC BY-SA 3.0","sideEffects": false,"main": "./cjs/index.js","module": "./esm/index.js","types": "./esm/index.d.ts","files": ["dist","src"],"scripts": {"build": "yarn build:cjs && yarn build:esm","build:cjs": "tsc -p tsconfig.cjs.json","build:esm": "tsc -p tsconfig.esm.json"}}
この例はdist
にトランスパイルした結果のjs, d.ts, d.ts.map
があり、src
に元のts
があるものと想定しています。
sourceMap
とはAltJSがトランスパイルされたJavaScriptとの行を一致させるものです。これがあることによってデバッグやトレースをしている時に、元のts
ファイルの何行目で問題が発生しているかわかりやすくなります。module bundler
を使用する時はこのオプションを有効にしていないと基本的に何もわかりません。このオプションはパッケージを公開しないとしても有効にしておくことが望ましいでしょう。
tsconfig.jsonにあるsourceMap
の項目をtrue
に変更します。
"sourceMap": true,/* Generates corresponding '.map' file. */
こちらもトランスパイルで出力したjs
ファイルと同じディレクトリに同名で拡張子がjs.map
のファイルも出力されるようになります。
フロントエンドでもバックエンドでもTypeScriptこれ一本!Universal JSという考えがあります。確かにフロントエンドを動的にしたいのであればほぼ避けて通れないJavaScriptと、バックエンドでも使えるようになったJavaScriptで同じコードを使いまわせれば保守の観点でも異なる言語を触る必要がなくなり、統一言語としての価値が大いにあります。
しかしながらフロントエンドとバックエンドではJavaScriptのモジュール解決の方法が異なります。この差異のために同じTypeScriptのコードを別々に分けなければいけないかというとそうではありません。ひとつのモジュールをcommonjs, esmodule
の両方に対応した出力をするDual Packageという考えがあります。
名前が仰々しいですが、やることはcommonjs
用のJavaScriptとesmodule
用のJavaScriptを出力することです。つまり出力するmodule
の分だけtsconfig.jsonを用意します。
プロジェクトはおおよそ次のような構成になります。
./├── tsconfig.base.json├── tsconfig.cjs.json├── tsconfig.esm.json└── tsconfig.json
tsconfig.base.json
基本となるtsconfig.jsonです
tsconfig.cjs.json
tsconfig.base.jsonを継承したcommonjs
用のtsconfig.jsonです
tsconfig.esm.json
tsconfig.base.jsonを継承したesmodule
用のtsconfig.jsonです
tsconfig.json
IDEはこの名前を優先して探すので、そのためのtsconfig.jsonです
tsconfig.base.jsonとtsconfig.jsonを分けるかどうかについては好みの範疇です。まとめてしまっても問題はありません。
tsconfig.jsonは他のtsconfig.jsonを継承する機能があります。上記はtsconfig.cjs.json, tsconfig.esm.jsonは次のようにしてtsconfig.base.jsonを継承しています。
// tsconfig.cjs.json{"extends": "./tsconfig.base.json","compilerOptions": {"module": "commonjs","outDir": "./dist/cjs",// ...}}
// tsconfig.esm.json{"extends": "./tsconfig.base.json","compilerOptions": {"module": "esnext","outDir": "./dist/esm",// ...}}
outDir
はトランスパイルしたjs
と、型定義ファイルを出力していれば(後述)それを出力するディレクトリを変更するオプションです。
このようなtsconfig.xxx.jsonができていれば、あとは次のようにファイル指定してトランスパイルをするだけです。
tsc -p tsconfig.cjs.jsontsc -p tsconfig.esm.json
package.jsonもDual Packageのための設定が必要です。
package.jsonにあるそのパッケージのエントリーポイントとなるファイルを指定する項目です。Dual Packageのときはここにcommonjs
のエントリーポイントとなるjs
ファイルを設定します。
Dual Packageのときはここにesmodule
のエントリーポイントとなるjs
ファイルを設定します。
型定義ファイルのエントリーポイントとなるts
ファイルを設定します。型定義ファイルを出力するようにしていればcommonjs, esmodule
のどちらのtsconfig.jsonで出力したものでも問題ありません。
package.jsonはこのようになっているでしょう。
{"name": "YYTS","version": "1.0.0","license": "CC BY-SA 3.0","main": "./cjs/index.js","module": "./esm/index.js","types": "./esm/index.d.ts","scripts": {"build": "yarn build:cjs && yarn build:esm","build:cjs": "tsc -p tsconfig.cjs.json","build:esm": "tsc -p tsconfig.esm.json"}}
トランスパイル後のjs
のファイルの出力先はあくまでも例です。tsconfig.jsonのoutDir
を変更すれば出力先を変更できるのでそちらを設定後、package.jsonでエントリーポイントとなるjs
ファイルの設定をしてください。
module bundler
の登場により、フロントエンドは今までのような<script>
でいろいろなjs
ファイルを読み込む方式に加えてを全部載せjs
にしてしまうという選択肢が増えました。この全部載せjs
は開発者としては自分ができるすべてをそのまま実行環境であるブラウザに持っていけるので楽になる一方、ひとつのjs
ファイルの容量が大きくなりすぎるという欠点があります。特にそれがSPA(Signle Page Application)だと問題です。SPAは読み込みが完了してから動作するのでユーザーにしばらく何もない画面を見せることになってしまいます。
この事態を避けるためにmodule bundler
は容量削減のための涙ぐましい努力を続けています。その機能のひとつとして題名のTree Shakingを紹介するとともに、開発者にできるTree Shaking対応パッケージの作り方を紹介します。
Tree Shakingとは使われていない関数、クラスを最終的なjs
ファイルに含めない機能のことです。使っていないのであれば入れる必要はない。というのは至極当然の結論ですがこのTree Shakingを使うための条件があります。
esmodule
で書かれている
副作用(side effects)のないコードである
各条件の詳細を見ていきましょう。
commonjs
とesmodule
では外部ファイルの解決方法が異なります。
commonjs
はrequire()
を使用します。require()
はファイルのどの行でも使用ができますがesmodule
のimport
はファイルの先頭でやらなければならないという決定的な違いがあります。
require()
はある時はこのjs
を、それ以外のときはあのjs
を、と読み込むファイルをコードで切り替えることができます。つまり、次のようなことができます。
let police = null;let firefighter = null;if (shouldCallPolice()) {police = require('./police');} else {firefighter = require('./firefighter');}
一方、先述のとおりesmodule
はコードに読み込みロジックを混ぜることはできません。
上記例でshouldCallPolice()
が常にtrue
を返すように作られていたとしてもmodule bundler
はそれを検知できない可能性があります。本来なら必要のないfirefighter
を読み込まないという選択を取ることは難しいでしょう。
最近ではcommonjs
でもTree Shakingができるmodule bundler
も登場しています。
ここで言及している副作用とは以下が挙げられます。
export
するだけで効果がある
プロトタイプ汚染のような、既存のものに対して影響を及ぼす
これらが含まれているかもしれないとmodule bundler
が判断するとTree Shakingの効率が落ちます。
module bundler
に制作したパッケージに副作用がないことを伝える方法があります。package.jsonにひとつ加えるだけで完了します。
このプロパティをpackage.jsonに加えて、値をfalse
とすればそのパッケージには副作用がないことを伝えられます。
{"name": "YYTS","version": "1.0.0","license": "CC BY-SA 3.0","sideEffects": false,"main": "./cjs/index.js","module": "./esm/index.js","types": "./esm/index.d.ts","scripts": {"build": "yarn build:cjs && yarn build:esm","build:cjs": "tsc -p tsconfig.cjs.json","build:esm": "tsc -p tsconfig.esm.json"}}
副作用があり、そのファイルが判明している時はそのファイルを指定します。
{"name": "YYTS","version": "1.0.0","license": "CC BY-SA 3.0","sideEffects": ["./xxx.js","./yyy.js"],"main": "./cjs/index.js","module": "./esm/index.js","types": "./esm/index.d.ts","scripts": {"build": "yarn build:cjs && yarn build:esm","build:cjs": "tsc -p tsconfig.cjs.json","build:esm": "tsc -p tsconfig.esm.json"}}
厳密なコーディングといえばlinter
があります。TypeScript自身にもより型チェックを厳密にするオプションがあります。以下はtsconfig.jsonの該当する部分を抜粋したものです。
/* Strict Type-Checking Options */"strict": true, /* Enable all strict type-checking options. */// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */// "strictNullChecks": true, /* Enable strict null checks. */// "strictFunctionTypes": true, /* Enable strict checking of function types. */// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. *//* Additional Checks */// "noUnusedLocals": true, /* Report errors on unused locals. */// "noUnusedParameters": true, /* Report errors on unused parameters. */// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
初期設定ではstrict
のみが有効になっています。
以下は各オプションの説明です。
このオプションはTypeScript4.0時点で次の7個のオプションをすべて有効にしていることと同じです。スクラッチから開発するのであれば有効にしておいて差し支えないでしょう。
noImplicitAny
strictNullChecks
strictFunctionTypes
strictBindCallApply
strictPropertyInitialization
noImplicitThis
alwaysStrict
この説明にTypeScriptのバージョンが明記されているのは、今後のバージョンでオプションが追加または廃止されることがありうるからです。より安定したオプションを設定したい場合はstrict
ではなく個々のオプションを有効にしてください。
型を明示しない引数はTypeScriptではany
型になりますが、これを禁止します。
function increment(i) {return i + 1;}
noImplicitAny
をtrue
に設定しこれをトランスパイルしようとすると
Parameter 'i' implicitly has an 'any' type.
となります。これを回避するためには
function increment(i: number): number {return i + 1;}
とします。なお戻り値の型は必須ではありません。
null, undefined
のチェックが厳密になります。このオプションを入れていると変数に代入する時にnull, undefined
の代入を防げます。
const error: Error = null;
strictNullChecks
をtrue
に設定しこれをトランスパイルしようとすると
Type 'null' is not assignable to type 'Error'.
となります。これを回避するためには
const error: Error | null = null;
とします。
関数の引数の型チェックが厳格になります。
class RuntimeError extends Error {private cause?: Error;public constructor(message: string, cause?: Error) {super(message);this.cause = cause;}public stacktrace(): string | undefined {if (typeof this.cause === 'undefined') {return this.stack;}return this.cause.stack;}}
Error
を拡張して他のError
を内包できるようにしたRuntimeError
があります。このクラスにはstacktrace()
というメソッドがあります。
スタックトレースを出力するメソッドruntimeDump()
を作成します。
const runtimeDump = (err: RuntimeError): void => {console.log(err.stacktrace());};
もちろん以下は動作します。
runtimeDump(new RuntimeError('runtime error', new Error('error')));
runtimeDump()
をError
型を引数に受けるType alias
に代入します。
type ErrorDump = (err: Error) => void;const dump: ErrorDump = runtimeDump;
代入したdump()
はもちろん引数にError
型を受けることができます。
dump(new Error('error'));
しかしながら、これは落ちてしまいます。
TypeError: err.stacktrace is not a function
これはError
にはerr.stacktrace()
が存在しないことが原因です。
strictFunctionTypes
をtrue
に設定しこれをトランスパイルしようとすると
Type '(err: RuntimeError) => void' is not assignable to type 'ErrorDump'.Types of parameters 'err' and 'err' are incompatible.Property 'stacktrace' is missing in type 'Error' but required in type 'RuntimeError'.
となります。関数の引数における型は、このオプションを有効にすることで代入時にそのクラスまたはサブクラス以外を禁止します。
function.bind(), function.call(), function.apply()
の型チェックが厳密になります。
function stackTrace(error: Error): string | undefined {return error.stack;}stackTrace.call(undefined, null);
strictBindCallApply
をtrue
に設定しこれをトランスパイルしようとすると
Argument of type 'null' is not assignable to parameter of type 'Error'.
となります。これを回避するためにはcall()
の第2引数をError
のインスタンスに変更します。
stackTrace.call(undefined, new ReferenceError());
初期化されていない変数定数や、コンストラクタで初期化されていないクラスのプロパティを禁止します。
class User {public name: string;public gender: string;public age: number;}
strictPropertyInitialization
をtrue
に設定しこれをトランスパイルしようとすると
Property 'name' has no initializer and is not definitely assigned in the constructor.Property 'gender' has no initializer and is not definitely assigned in the constructor.Property 'age' has no initializer and is not definitely assigned in the constructor.
となります。これを回避するためにはコンストラクタで初期化するか、初期値を設定します。
class User {public name: string;public gender: string;public age: number;public constructor(name: string, gender: string, age: number) {this.name = name;this.gender = gender;this.age = age;}}
class User {public name: string = 'John';public gender: string = 'Female';public age: number = 20;}
このオプションが有効だと、ORMで使うようなプロパティがすべてpublic
でコンストラクタのないクラスは基本的に作れなくなります。
名前付き関数、匿名関数はアロー関数と異なり、実行時にthis
が決定されます。そのため、内部でthis
を使っているとそれは関数を書いている時点ではany
型と同じ扱いになります。このオプションはそれを禁止します。
次のようなType alias
があるとします。
type Person = {name01: string;name02: string;name03: string;name04: string;name05: string;name06: string;name07: string;name08: string;name09: string;name10: string;name11: string;name12: string;name13: string;name14: string;name15: string;name16: string;name17: string;name18: string;name19: string;name20: string;intro(): void;};
そして、アルファベットの先頭1文字を大文字にするcapitalize()
とnameXX
のプロパティを出力するdump()
を定義します。
function capitalize(str: string): string {return `${str[0].toUpperCase()}${str.slice(1)}`;}function dump(): string {const props: string[] = [];props.push(capitalize(this.name01));props.push(capitalize(this.name02));props.push(capitalize(this.name03));props.push(capitalize(this.name04));props.push(capitalize(this.name05));props.push(capitalize(this.name06));props.push(capitalize(this.name07));props.push(capitalize(this.name08));props.push(capitalize(this.name09));props.push(capitalize(this.name10));props.push(capitalize(this.name11));props.push(capitalize(this.name12));props.push(capitalize(this.name13));props.push(capitalize(this.name14));props.push(capitalize(this.name15));props.push(capitalize(this.name16));props.push(capitalize(this.name17));props.push(capitalize(this.name18));props.push(capitalize(this.name19));props.push(capitalize(this.name20));props.push(capitalize(this.name21));props.push(capitalize(this.name22));props.push(capitalize(this.name23));props.push(capitalize(this.name24));return props.join(' ');}
これを使いPerson
のインスタンスを作成します。
const person: Person = {name01: 'pablo',name02: 'diego',name03: 'josé',name04: 'francisco',name05: 'de',name06: 'paula',name07: 'juan',name08: 'nepomuceno',name09: 'maría',name10: 'de',name11: 'los',name12: 'remedios',name13: 'cipriano',name14: 'de',name15: 'la',name16: 'santísima',name17: 'trinidad',name18: 'ruiz',name19: 'y',name20: 'picasso',intro: dump};
トランスパイルして実行します。
console.log(person.intro());
落ちます。
TypeError: Cannot read property '0' of undefined
これはプロパティがname20
までしかないPerson
のオブジェクトリテラルに対してname21 ~ name24
のプロパティを取得し、それにcapitalize()
を適用しようとしたことが問題です。
noImplicitThis
をtrue
に設定しこれをトランスパイルしようとすると大量の次の警告が表示されます。
'this' implicitly has type 'any' because it does not have a type annotation.
これを回避するためにはdump()
の関数が扱っているthis
が何かを指定します。dump()
の第1引数をthis
とし、その型を書くことでTypeScriptに伝えることができます。
function dump(this: Person): string {// ...}
するとTypeScriptは存在しないプロパティについての指摘をするようになります。name21 ~ name24
に次の警告が出るようになります。
Property 'nameXX' does not exist on type 'Person'. Did you mean 'name01'?
この引数のthis
については関数のページに詳細がありますので併せてご参照ください。
'use strict'
を各ファイルの先頭に付加します。
使用していない変数を禁止します。
function add(n1: string, n2: string): number {const str: string = 'this is debug message';// debug(str);return n1 + n2;}
noUnusedLocals
をtrue
に設定しこれをトランスパイルしようとすると
'str' is declared but its value is never read.
となります。デバッグをしている時など若干邪魔なときがあります。
使用していない引数を禁止します。
function choose(n1: string, n2: string): number {return n2;}
noUnusedParameters
をtrue
に設定しこれをトランスパイルしようとすると
'n1' is declared but its value is never read.
となります。これは上記例のように第2引数だけを使用する関数に対しても適用されます。これを回避するためには、使用していない引数を_
で始まる名前に変更します。
function choose(_n1: string, n2: string): number {// ...}
関数のすべての条件分岐でreturn
が行われているかを厳密にチェックします。
function negaposi(num: number): string {if (num > 0) {return 'positive';} else if (num < 0) {return 'negative';}}
noImplicitReturns
をtrue
に設定しこれをトランスパイルしようとすると
Not all code paths return a value.
となります。これを回避するためには戻り値がvoid
型以外の関数は常に最後にreturn
を書くようにするか、場合分けを漏れないように設計するしかありません。
function negaposi(num: number): string {if (num > 0) {return 'negative';} else if (num < 0) {return 'positive';}return '0';}
fallthrough
とはswitch
でbreak
またはreturn
を行わないことを意味します。以下は多くの方がswitch
で学習したであろうfallthough
の例です。
function daysOfMonth(month: number): number {switch (month) {case 1:case 3:case 5:case 7:case 8:case 10:case 12:return 31;case 2:return 28;case 4:case 6:case 9:case 11:return 30;default:throw new Error('INVALID INPUT');}}
意図してこのfallthrough
を使いこなすよりもバグを産むことの方が遥かに多いため、このオプションはそれを禁止します。
function nextLyric(lyric: string, count: number = 1): string {switch (lyric) {case 'we':return 'will';case 'will':if (count === 1) {return 'we';}if (count === 2) {return 'rock';}case 'rock':return 'you';default:throw new Error('YOU ARE A KING!!!');}}
noFallthroughCasesInSwitch
をtrue
に設定しこれをトランスパイルしようとすると
Fallthrough case in switch.
となります。これはcase 'will'
のときにreturn
されない場合がある、つまりfallthrough
が発生していることが問題です。
これを回避するためにはcase
では漏れなくbreak
あるいはreturn
をするように設計します。
function next(lyric: string, count: number): string {switch (lyric) {case 'we':return 'will';case 'will':if (count % 2 === 1) {return 'we';}return 'rock';case 'rock':return 'you';default:throw new Error('YOU ARE A KING!!!');}}
なお、このオプションはcase
に処理がある場合のみbreak
あるいはreturn
を強制します。この項目で一番初めに紹介した一か月の日数を求める関数daysOfMonth()
は、fallthrough
であるcase
はすべて処理がないため警告は発生しません。
インデックス型や配列で宣言されたオブジェクトが持つプロパティへのアクセスが厳密になります。インデックス型についてはタイプエイリアスのページをご参照ください。
type ObjectLiteralLike = {[key: string]: string;};type ArrayObjectLike = {[key: number]: string;};const butterfly: ObjectLiteralLike = {en: 'Butterfly',fr: 'Papillon',it: 'Farfalla',es: 'Mariposa'};const phoneticCodes: ArrayObjectLike = {0: 'alpha',1: 'bravo',2: 'charlie'};
ObjectLiteralLike, ArrrayObjectLike
は共にstring
型のプロパティを持つオブジェクトの型として宣言されています。
const germanName: string = butterfly.de;const fifth: string = phoneticCodes[4];
これらのオブジェクトのプロパティにアクセスする時完全な型安全ではありません。上記germanName, fifth
はどちらも定義されたオブジェクトには存在しませんがTypeScriptaはこれらをstring
型と解釈します。
noUncheckedIndexedAccess
をtrue
に設定しこれらをトランスパイルしようとすると
Type 'string | undefined' is not assignable to type 'string'.Type 'undefined' is not assignable to type 'string'.
このように厳密に定義されていないプロパティはundefined
型とのユニオン型として解釈されるようになります。
const englishName: string | undefined = butterfly.en;const first: string | undefined = phoneticCode[0];
ここであるサービスが英語版だけは担保し、他の言語は追々という対応をしたとします。するとそのシステムにある単語や文章を意味する型は次のようになります。
type SystemTerms = {[key: string]: string;en: string;};
このような型を定義するとそのオブジェクトはen
プロパティに限りnoUncheckedIndexedAccess
の制約を受けません。
const butterfly: SystemTerms = {en: 'Butterfly',fr: 'Papillon',it: 'Farfalla',es: 'Mariposa'};const englishName: string = butterfly.en;const frenchhName: string | undefined = butterfly.fr;
配列はインデックスを指定する方法でアクセスをするとundefined
型とのユニオン型と解釈されますがfor-of, array.forEach()
はこの制約を受けないため積極的に使用を検討してください。
const phoneticCodes: string[] = ['alpha', 'bravo', 'charlie'];for (const p of phoneticCodes) {// ...}phoneticCodes.forEach((p: string) => {// ...});