TypeScript を使いこなして手に入れる型安全 -『TypeScript』
- uto usui
- //
- javascript
- typescript
TypeScriptでは型を絞り込んで行く作業が、バグを減らしていくことにつながります。typeof
など、JavaScript の従来の方法を取り入れつつ TypeScript を使いこなして型安全を実現します。
アノテーションによる型安全
undefined
や null
な値の振る舞いや、引数の存在の有無をアノテーションの記述によって対応していく方法で型安全を実現します。
関数と nullable 型
関数を実行するとき、引数の有無が考えられる場合に、引数が与えられるなら任意の型を与えられないなら null
であることを期待するように記述します。このパターンを Nullable 型と呼びます。
const apply3d2y = (name: string, age: number | null = null) => {
if (age === null) return { name, age: 2 }
return {
name,
age: age + 2,
}
}
上のコードでは null
かどうかを if state でチェックして、 age を number
として絞り込んでから算術計算しているので、型安全が保証されています。TypeScript の記述では、このように絞り込むということにフォーカスして書くと型安全なエラーのないコードを実現できます。
また augument?: type
というように、 ?
で任意の型か undefind
かというように引数の型を表現して、 null
ではなく undefined
を if state でチェックする方法もありますが、デフォルト引数で表現した方が null
以外への拡張も容易なのでこちらをベースでいいかなとおもいます。
week type / excess property checks
すべてのプロパティが ?
でオプショナルなオブジェクト型を Weak Type と呼びます。Weak Type はまったく異なるプロパティのオブジェクトを代入するとエラーに、ひとつでも当てはまるとスルーという挙動をします。意図していないオブジェクトの代入をチェックする機構です。
interface Member {
name?: string
age?: number
}
const acceptMember = (member: Member) => {
console.log(member)
}
const ruffy = {
name: 'Monkey',
age: 19,
sex: 'male',
}
const teach = {
_name: 'black beard',
sex: 'male',
}
acceptMember(ruffy)
acceptMember(teach) // error
// excess prop check
acceptMember({
name: 'Monkey',
age: 19,
sex: 'male', // error
})
変数宣言せず関数に直接オブジェクトを与えると、excess property check という機構が働いて、定義されたプロパティ以外を持っていることを検知して警告してきます。
Readonly
プロパティに readonly を与えると読み取り専用になりますが、すべてのプロパティに readonly を与えたい場合は、オブジェクトを Readonly 型で指定します。
interface Member {
name: string
age: number
}
const ruffy: Readonly<Member> = {
name: 'Monkey',
age: 19,
}
ruffy.age = 17 // error
抽象度をコントロールして型安全
型の抽象度を詳細または抽象的に、間口をコントロールすることで安全性を担保できます。
ダウンキャスト
TypeScript の型推論に頼るのではなく、さらに実装者が型の詳細を指定したいとき、 絞り込むために、アサーションを使います。このことをダウンキャストと呼び、型の互換性があれば成立します。記法は2つあります。
const el = document.querySelector<HTMLDivElement>('#el')
const canvas = document.querySelector('#canvas) as HTMLCanvasElement'
ダウンキャストの逆でアップキャストという概念もあります。型推論に any を与えるなどすることがアップキャストと呼びますが、ダウンキャストとは利点も逆で、型の詳細がわからなくなるため危険な実装なので、慎重に検討します。
インデックスシグネチャ
型が付与されているオブジェクトに、定義されていないプロパティを代入するとエラーになります。オブジェクトに動的な値を追加するには [key: type]
というように記述し、これをインデックスシグネチャと呼びます。必須のプロパティがあるなら、従来通りメンバーとして登録すればよいです。
// error
interface Member {
name: string
age: number
}
const ruffy: Member = {
name: `Monkey`,
age: 19,
sex: `male`,
}
// ok
interface Member {
name: string
[key: string]: string | number
}
const ruffy: Member = {
name: `Monkey`,
age: 19,
sex: `male`,
}
ruffy.name // string
ruffy.sex // string | number
[key: string]: string | number
で動的な key 名と string
と number
型のメンバーに対応したオブジェクトを定義しています。
プロパティ値の型を制限して、与えられる値を限られたものにします。未定義の場合も推論できるようにしておきます。
type Names = 'Nami' | 'Ruffy' | 'Zoro'
interface Member {
from: {
[key: string]: Names | undefined
}
}
const mugiwara: Member = {
from: {
kokoyashi: 'Nami',
fusya: 'Ruffy',
shimotsuki: 'Zoro',
baratie: 'Sanji', // error
},
}
プロパティキーの型をin
で指定して制限します。未定義を許容するには ?
で表現できます。
type Names = 'Nami' | 'Ruffy' | 'Zoro'
interface Member {
from: {
[key in Names]: string
}
}
const mugiwara: Member = {
from: {
Nami: 'kokoyashi',
Ruffy: 'fusya',
Zoro: 'shimotsuki',
Sanji: 'baratie', // error
},
}
const assertion
const assertion を利用すると tuple
型を簡略化して記述できます。変数を宣言したときの値をそのまま tuple
として適用します。オブジェクトや配列の定数を表現するのが楽です。
''''const ruffy = [Monkey, 19] as const // readonly [Monkey, 19]
絞り込んで型安全
JavaScript で従来行っていた型チェックの構文を利用して、型を絞り込む方法があります。
typeof type guards
引数の union types を typeof
で型チェックをして return
して絞り込むシンプルな方法です。
const typeCheck = (val: number | string) => {
if (typeof val === 'number') {
return ['number', val]
}
return ['string', val]
}
typeCheck(0)
typeCheck(name)''''
型推論で 2つ目の return
で val は string
に推論されます。
in type guards
引数に渡されるオブジェクトの union type を、どちらかのみに存在するプロパティを in
演算子で比較すると、型の絞り込みが可能です。
type Ruffy = {
gender: 'male'
age: 19
}
type Nami = {
gender: 'female'
}
const checkMember = (member: Ruffy | Nami) => {
if ('age' in member) return member // Ruffy
return member // Nami
}
instanceof type guards
instanceof
では、引数が class の型を絞り込めます。
class Ruffy {
private age: number
constructor() {
this.age = 19
}
}
class Nami {
private gender: string
constructor() {
this.gender = 'female'
}
}
const checkMember = (member: Ruffy | Nami) => {
if (member instanceof Ruffy) return member // Ruffy
return member // Nami
}
Tagged Union types
引数の Union types すべてが共通のプロパティを持っていてリテラルの場合、条件分岐で型を絞り込め、この条件に合う Union types を Tagged Union おと呼びます。
type Ruffy = {
gender: 'male'
}
type Nami = {
gender: 'female'
}
const checkMember = (member: Ruffy | Nami) => {
if (member.gender === 'male') return member // Ruffy
return member // Nami
}
User type guard
匿名関数の戻り型アノテーションとして is
を利用し、ユーザーが直接絞り込んで真偽値を返す関数を定義できます。
type Ruffy = {
age: 19
[k: string]: any
}
type Nami = {
gender: 'female'
[k: string]: any
}
const checkRuffy = (member: Ruffy | Nami): member is Ruffy => {
return member.age !== undefined
}
const checkNami = (member: Ruffy | Nami): member is Ruffy => {
return member.gender !== undefined
}
const checkUser = member => {
if (checkRuffy(member)) {
return member // Ruffy
} else if (checkNami(member)) {
return member // Nami
} else {
return member // any
}
}
is
を使うことによって、アップキャスト的に実装者がその型であることを保証して絞り込んでいるので、TypeScript の型推論が実装者頼りになる、ということを意識しておく必要があります。
おわります。