テックタッチアドベントカレンダー5日目を担当する@takakobemです。
4日目は @terunuma による 4Kモニタ環境で1年間Web開発してみた所感 でした。
4Kモニタいいですよね。ただ、以前私は43型の4Kモニタを使っていたのですが、正直でかすぎて画面の端が見づらかったです。。30インチぐらいがベストかもしれませんね。
まえがき
本記事はTypescriptを触ったことがある人を対象にしています。
「Typescriptは使っているけど、ちゃんと使いこなせているかわからない」という人に一番しっくりくる内容だと思います。
私が以前C++を触っていたこともあり、結構C++を引き合いに出しています。
また、私がReactやReduxを触っていることもあり、オブジェクト指向よりも関数型プログラミングを意識したものになっています。
はじめに
Typescriptは非常に便利なツールです。
Javascriptそれ自体は静的な型チェックができないため、コーディング中に実装ミスに気づきにくいのですが、Typescriptを導入することで型チェックが効くようになり、VSCodeなどのエディターを使えば補完も利用できるようになります。
ただ、TypescriptはC++やJavaなどといった言語とは違い、型の定義が中途半端でも動いてしまいます。
一見型をしっかりつけているつもりで書いていても、ちょっとしたことで信頼性がないコードができあがってしまいます。
今回は型を妥協してしまっている書き方をBadケースとして例に上げ、どうすればよいかをGoodケースとして紹介していきます。
※本記事はTypescript3.7時点のものです。
1. anyは使わない
Typescriptを触ったことがある人であれば、any
は知っているかと思います。
極端に言ってしまうと、any
は型を付けるのを放棄したのと同義です。
Typescript は Javascript に型を付けるための言語なのに、型を付けるのを放棄してしまっては元も子もありません。
Typescript で型を付けることを選んだのであれば、any
を使うことは避けましょう。
Bad
number
を引数に取りたいのに、any
にしてしまうとstring
型でも通ってしまいます。
declare function hoge(num: any): void
hoge(1) // ok
hoge("hoge") // これもokになってしまう
Good
ちゃんと型を定義してあげましょう。
declare function fuga(num: number): void
fuga(1) // ok
fuga("fuga") // error
2. !
は使わない
!
はnon-null assertion operatorと呼ばれています。
これをつけることで、null
かもしれないようなものを強制的にnull
じゃないものとしてみなすことができます。
「ここはnull
はありえないはずだから〜」という理由でよく使いがちなのですが、ありえないと思ってしまっているだけかもしれませんし、今後null
が入ってしまっても気づけなくなりますよね。
なので、そもそもnull
をとらないようにしてしまうか、ちゃんと実行時にチェックをするべきです。
Typescript3.7からの機能であるOptional Chainingを使うのもありです。
Bad
function hoge(str: string | null) {
str.toUpperCase() // Object is possibly 'null'.
str!.toUpperCase() // ok
}
Good
ちゃんと実行時にチェックしてあげましょう。
もしくはnullをとらないようにしてあげましょう。
function hoge(str: string | null) {
if (str !== null) {
str.toUpperCase() // ok
}
}
// or
function fuga(str: string | null) {
str?.toUpperCase() // ok
}
// or
function fuga(str: string) {
str.toUpperCase() // ok
}
3. {}
を使う時は注意
例えばkeyに名前を持ち、valueに年齢を持つようなオブジェクトを定義したいとします。
keyは名前だからstring
, valueは年齢だからnumber
と安直にやってしまうと、型安全が崩れてしまいます。
Bad
安直に上記の通りに定義してみます。
const ageData: Record<string, number> = {
yamada: 10,
tanaka: 20
}
console.log("山田さんの年齢=", ageData.yamada) // 10
console.log("田中さんの年齢=", ageData.tanaka) // 20
console.log("鈴木さんの年齢=", ageData.suzuki) // undefined
この時、山田さんのデータは入っていますが、鈴木さんのデータは入っていないため、鈴木さんの年齢はundefined
として返ってきます。
しかし、Typescript的にはエラーと認識してくれません。
Good
keyが予め予測可能なものであれば、keyを定義しておきましょう。
const ageData: Record<"yamada" | "tanaka", number> = {
yamada: 10,
tanaka: 20
}
console.log("鈴木さんの年齢=", ageData.suzuki) // Property 'suzuki' does not exist on type 'Record<"yamada" | "tanaka", number>'.(2339)
idなど、keyが事前に定義不能な場合は、valueにundefined
も定義しておきましょう。
const ageData: Record<string, number | undefined> = {
yamada_id: 10,
tanaka_id: 20
}
console.log("鈴木さんの年齢=", ageData.suzuki + 1) // Object is possibly 'undefined'.
4. as
は使わない
以下のような定義があったとしましょう。
type Base = {
x: number
}
type Derived = Base & {
y: number
}
これは、オブジェクト指向でいう基底クラスと派生クラスの関係に近いです。
Typescript では、このように Intersection Types を使うことで継承のようなものを表現することができます。
ここで問題なのが、as
を使うことでこれら2つの型が双方向にキャストできてしまうということです。
Bad
例えば、以下のようなアップキャストは問題なく行なえます。
const derived: Derived = {
x: 1,
y: 2
}
const base = derived as Base
console.log(base.x) // 1
元のderived
にはx
が含まれているので何も問題ありませんね。
次はダウンキャストです。
const base: Base = {
x: 1,
}
const derived = base as Derived // ダウンキャストができてしまう
console.log(derived.y) // undefined
base
にはy
が無いのに、エラーなく実行されてしまいます。
as
を使うとダウンキャストがすんなりできてしまいます。
Good
ではどうすればよいのか。
いえ、どうもしなくていいんです。
そもそもTypescriptが提供するものはただの型です。
継承といっても、カプセル化や関数のオーバーライドなどはありません。
そもそもキャストする必要がないのです。
ただ、引数に基底クラスを受け、条件に応じて派生クラスに変換し結果を返したいようなケースはありますよね。それを次に説明します。
5. Union Types + String Literalを使いこなす
以下のような、CircleとSquareという型があるとします。
type Circle = {
radius: number
}
type Square = {
height: number
width: number
}
これはどちらも図形ですね。Typescriptでは以下のようにUnion Typesを使うことで、異なる2つの型を1つの型として扱うことができます。
type Figure = Circle | Square
これは、派生クラスと派生クラスから基底クラスを作るようなものです。
C++から入った僕は非常に理解に苦しみました。
Typescript恐ろしい。。
Bad
このようにすることで、例えば以下のように図形を引数にその面積を返すような関数が定義できます。
function area(figure: Figure): number {
if ("height" in figure) {
return figure.width * figure.height
}
if ("radius" in figure) {
return figure.radius * figure.radius * Math.PI
}
throw new Error("no such figure")
}
あれ?なんか奇妙ですよね。
そう、上記の例では、図形に特定のプロパティがあるかどうかで図形を判定し、面積を計算しています。
でもこれって気持ち悪いですよね。例えばTriangle
という型が追加になると、Triangle
もheight
を持つので、条件式を見直さなければならなくなります。
「クラスを使ってメンバ関数で計算しろよ」って言われそうですが、クラスにしなくてもオブジェクトにちょこっと細工することで解決できちゃうんです。
(なぜクラスを頑なに使わないのか、React/Redux使いの人なら知ってるかと思いますが、その理由はまたいつか機会があれば記事書きます。)
Good
ではどうするかというと、こういう時は その型が何か を string literal で定義しておけばよいです。
type Circle = {
kind: "circle"
radius: number
}
type Square = {
kind: "square"
height: number
width: number
}
こうすることで、プロパティの存在によってif文を分けなくても、kind
というkeyによってswitch文で処理を分けることができるようになります。
また、例えば case "circle"
内で figure.width
にアクセスしようとするとエラーになります。
function area(figure: Figure): number {
switch (figure.kind) {
case "circle":
// figure.width // Property 'width' does not exist on type 'Circle'.
return figure.radius * figure.radius * Math.PI
case "square":
return figure.height * figure.width
}
}
Typescript賢すぎる!
最後に
いかがでしたでしょうか。
Typescriptは非常に便利なツールですが、使いこなすにはいくつかコツがいりますし、正直難しいです。
今回挙げた内容もTypescriptができることのほんの一部に過ぎません。
しかし、使いこなせれば非常に便利なツールです。こんなことできるかな?と思ったことが大抵できますので、是非いろいろ試してみてください。
6日目は @ihiroky による「JSXとvirtual-domで遊ぶ」です。