想定対象読者
- JavaScriptある程度書ける人
- TypeScript書いたことが無い、または始めたばかりの人
環境
- TypeScript 3.6.3
- strict: true
本校に掲載されているソースコードは TypeScript Playground に貼り付けることで、実際の動作を試すことができます。
TypeScriptの幸せの定義
TypeScriptにおける幸せとは、
JavaScriptだったらランタイムエラーになるコードが
TypeScriptにしたおかげで、コンパイルエラーとして開発中に検知できること
です。
例を見てみます。
下記JavaScriptのプログラムは、文字列ではないthisIsNumber
に対してtoUpperCase()
を呼んでしまっており、実行時エラーになります。
const thisIsString = "1";
const thisIsNumber = 1;
thisIsString.toUpperCase(); // OK
thisIsNumber.toUpperCase(); // ランタイムエラー
一方TypeScriptの下記プログラムでは、number型に対してtoUpperCase()
が存在しないことが静的型解析によって事前に分かるため、コンパイル時にエラーとなります。
const thisIsString: string = "1";
const thisIsNumber: number = 1;
thisIsString.toUpperCase(); // OK
thisIsNumber.toUpperCase(); // コンパイルエラー(Property 'toUpperCase' does not exist on type 'number'.)
このようにコンパイルエラーとして開発中に検知できるようにすることが、TypeScriptとの上手な付き合い方です。
不幸せになる方法
any型を使うことで、いとも簡単に幸せを手放すことができます。
const thisIsNumber: any = 1;
thisIsNumber.toUpperCase(); // コンパイルエラーは出ず、ランタイムエラー
thisIsNumber
をnumber型としていたらコンパイルエラーとして検知できていたのに、any型として定義してしまった場合はコンパイラの型チェックをパスしてしまいます。
しかし、any型を使うのはやむを得ないときもあります。
any型のコンパイルが通るようになるという強力な効果はJavaScriptの記法との互換性を獲得できるため、使うことで一時的にでも生産性が上がるのは否めません。
以下本稿では、やむを得ない時を少しでも減らしてTypeScriptで幸せになれる方法を、紹介していきます。
幸せになるためのTips
Union Types
型定義を記述しようにも「文字列か数値かどっちになるか分からない引数・変数」が出てくる時があると思います。
やむを得ずany
を使いたくなります。
const thisIsStringOrNumber: any = "1";
any
を使っていても、下記のようにtypeof
を使うことで型を絞り込むことができ、型安全に使えるケースもありますが...
// ※`declare`は今回の本質ではないので気にしないでください
declare const thisIsStringOrNumber: any;
if (typeof thisIsStringOrNumber === "string") {
// string型と推論されるので、String.prototype.toUpperCase()を使ってもコンパイルOK!
thisIsStringOrNumber.toUpperCase();
} else if (typeof thisIsStringOrNumber === "number") {
// number型と推論されるので、Number.prototype.toFixed()を使ってもコンパイルOK!
thisIsStringOrNumber.toFixed();
} else if (typeof thisIsStringOrNumber === "boolean") {
// ここを通ることはあるのか?
} else {
// ここを通ることはあるのか?
}
ここではUnion Typesを使います。
Advanced Types#Union Types · TypeScript
Union Typesを使うと、複数の型の和を表現できます。
この場合は「string
かnumber
のどちらか」という型をstring | number
で表現できます。
declare const thisIsStringOrNumber: string | number;
if (typeof thisIsStringOrNumber === "string") {
// string型と推論されるので、コンパイルOK!
thisIsStringOrNumber.toUpperCase();
} else {
// これだけでnumber型と推論されるので、コンパイルOK!
thisIsStringOrNumber.toFixed();
}
else
の部分で、typeof thisIsStringOrNumber === "number"
といった条件文を書いていないのにnumber型として変数を扱うことができている点が重要です。
Unknown Type
たとえばJSONを文字列として受け取るとき
const val1 = JSON.parse('1');
const val2 = JSON.parse('2');
console.log(val1 + val2); //=> stringの'12' か numberの3 かは分からないが、エラーは出ない
val1.toFixed();
val2.toUpperCase(); // ここでランタイムエラー
意図していない型で演算しているときはコンパイルエラーになって欲しいですが、エラーが出ずランタイムエラーになってしまいます。
これは、TypeScriptにおけるJSON.parse
の型定義がany
を返すようになっている事が原因です。
JSON.parse(text: string): any
何が返ってくるかわからない値に対して、any
を使わずコンパイルエラーが出るようにしたいです。
こういったケースの対策として、unknown型が導入されました。
TypeScript 3.0#New unknown top type · TypeScript
unknown
はどんな値にも適用できるという点ではany
と同様ですが、型が判明するまではいかなる演算にも使うことができずコンパイルエラーになる、という点が異なります。
const val1: unknown = JSON.parse('1');
const val2: unknown = JSON.parse('2');
console.log(val1 + val2); //=> コンパイルエラー
if (typeof val1 === "number" && typeof val2 === "number") {
// OK!
console.log(val1 + val2);
} else if (typeof val1 === "string" && typeof val2 === "string") {
// OK!
console.log(`${val1}${val2}`);
}
Literal Types
たとえば下記のようなOffice製品を文字列として引数に取る関数に、Keynote
という知らない製品が渡されたときのことを考えます。
const func = function(p: string) {
if (p === 'Word') return 'わーぷろ!';
if (p === 'Excel') return '表計算!';
if (p === 'PowerPoint') return 'プレゼン!';
throw new Error('予期しない製品です');
}
func('Word'); //=> わーぷろ!
func('Excel'); //=> 表計算!
func('Keynote'); //=> ランタイムエラー(予期しない製品です)
これは、引数が'Word'
,'Excel'
,'PowerPoint'
という3種類しか期待していないのに、string
型となっており無数の文字列を受け付けるようになっているのが、ランタイムエラーの原因です。
ここでは'Word'
,'Excel'
,'PowerPoint'
というString Literalを定義し、それらのUnion Typesを引数の型として指定することで、予期しない引数をコンパイルエラーで検知できます。
Advanced Types#String Literal Types · TypeScript
type Office = 'Word' | 'Excel' | 'PowerPoint';
const func = function(p: Office) {
if (p === 'Word') return 'わーぷろ!';
if (p === 'Excel') return '表計算!';
if (p === 'PowerPoint') return 'プレゼン!';
throw new Error('予期しない製品です');
}
func('Word'); //=> わーぷろ!
func('Excel'); //=> 表計算!
func('Keynote'); //=> コンパイルエラー
Index Type Query
本稿の話の応用編です。
下記のようなオブジェクトを考えてみます。
type Props = {
name: string;
age: string;
gender: string;
}
const obj: Props = { name: "suzuki", age: "28", gender: "male" };
const describe = function (key: string) {
// any使わざるを得ない...?
return `${key} is ${(obj as any)[key]}`;
};
describe('name'); // name is suzuki
describe('age'); // age is 28
describe('job'); // jobは無いので、job is undefined となってしまう
obj[key]
が何か分からないのでanyでキャストしないとコンパイルエラーになる点と、obj["job"]
は存在せずundefinedになってしまう点が不幸せなところです。
関数の引数がstring
型になっているため、これをString Literail Typesとして絞り込みたいです。そのまま"name" | "age" | "gender"
と書くことでもランタイムエラーを回避できるのですが、Propsの定義とダブルメンテになってしまいます。
ここでkeyofを使うと、定義済の型のキーをString Literal
のUnion Types
として得ることができます。
TypeScript 2.1#Index Type Query · TypeScript
type Props = {
name: string;
age: string;
gender: string;
}
const obj: Props = { name: "suzuki", age: "28", gender: "male" };
// key: "name" | "age" | "gender" と推論される
const describe = function (key: keyof Props) {
return `${key} is ${obj[key]}`;
};
describe('name'); // name is suzuki
describe('age'); // age is 2
describe('job'); // コンパイルエラー(Argument of type '"job"' is not assignable to parameter of type '"name" | "age" | "gender"')
ReactでPropsの型定義をした時などに有効です。Material-UIのドキュメントのTypeScriptサンプルコードでも使われている方法です。
interface State {
name: string;
age: string;
multiline: string;
currency: string;
}
export default function TextFields() {
// 中略
const handleChange = (name: keyof State) => (event: React.ChangeEvent<HTMLInputElement>) => {
setValues({ ...values, [name]: event.target.value });
};
}
TypeScriptで幸せになろう!