Edited at

ちょっとだけ幸せになれるTypeScript


想定対象読者


  • JavaScriptある程度書ける人

  • TypeScript書いたことが無い、または始めたばかりの人


環境


  • TypeScript 3.6.3


    • strict: true



本校に掲載されているソースコードは TypeScript Playground に貼り付けることで、実際の動作を試すことができます。


TypeScriptの幸せの定義

TypeScriptにおける幸せとは、

JavaScriptだったらランタイムエラーになるコードが

TypeScriptにしたおかげで、コンパイルエラーとして開発中に検知できること

です。

例を見てみます。

下記JavaScriptのプログラムは、文字列ではないthisIsNumberに対してtoUpperCase()を呼んでしまっており、実行時エラーになります。


JavaScript

const thisIsString = "1";

const thisIsNumber = 1;

thisIsString.toUpperCase(); // OK
thisIsNumber.toUpperCase(); // ランタイムエラー


一方TypeScriptの下記プログラムでは、number型に対してtoUpperCase()が存在しないことが静的型解析によって事前に分かるため、コンパイル時にエラーとなります。


TypeScript

const thisIsString: string = "1";

const thisIsNumber: number = 1;

thisIsString.toUpperCase(); // OK
thisIsNumber.toUpperCase(); // コンパイルエラー(Property 'toUpperCase' does not exist on type 'number'.)


このようにコンパイルエラーとして開発中に検知できるようにすることが、TypeScriptとの上手な付き合い方です。


不幸せになる方法

any型を使うことで、いとも簡単に幸せを手放すことができます。

Basic Types#any · TypeScript


TypeScript

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を使うと、複数の型の和を表現できます。

この場合は「stringnumberのどちらか」という型を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 LiteralUnion 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サンプルコードでも使われている方法です。

引用: https://github.com/mui-org/material-ui/blob/v4.5.1/docs/src/pages/components/text-fields/TextFields.tsx


ocs/src/pages/components/text-fields/TextFields.tsx

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で幸せになろう!


参考文献