16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

妥協しないTypescript

Last updated at Posted at 2019-12-04

テックタッチアドベントカレンダー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 :cry:

numberを引数に取りたいのに、anyにしてしまうとstring型でも通ってしまいます。

declare function hoge(num: any): void
hoge(1) // ok
hoge("hoge") // これもokになってしまう

Good :smile:

ちゃんと型を定義してあげましょう。

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 :cry:

function hoge(str: string | null) {
  str.toUpperCase() // Object is possibly 'null'.
  str!.toUpperCase() // ok
}

Good :smile:

ちゃんと実行時にチェックしてあげましょう。
もしくは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 :cry:

安直に上記の通りに定義してみます。

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 :smile:

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 :cry:

例えば、以下のようなアップキャストは問題なく行なえます。

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 :smile:

ではどうすればよいのか。
いえ、どうもしなくていいんです。
そもそも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 :cry:

このようにすることで、例えば以下のように図形を引数にその面積を返すような関数が定義できます。

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という型が追加になると、Triangleheightを持つので、条件式を見直さなければならなくなります。
「クラスを使ってメンバ関数で計算しろよ」って言われそうですが、クラスにしなくてもオブジェクトにちょこっと細工することで解決できちゃうんです。
(なぜクラスを頑なに使わないのか、React/Redux使いの人なら知ってるかと思いますが、その理由はまたいつか機会があれば記事書きます。)

Good :smile:

ではどうするかというと、こういう時は その型が何か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で遊ぶ」です。

16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?