地獄からの使者、その名はTypeScript ~ 固かった型の形 ~
TypeScriptは便利である。変な書き方をしていれば教えてくれるし、開発環境が入力補完の支援もしてくれる。まさにヘブンである。しかし書き方を知らないと、その世界は地獄と化す。真っ赤なエラーに焼き尽くされた地獄に。
地獄を回避するためには解決法を知ることである。TypeScriptは今も刻一刻と進化しており、昨日の知識は原始の人間が棒の握り方を覚えたに等しい。
ということで地獄を渡り、天国へ到達するための知恵の一部を紹介していきたいと思う。たぶん他にもあるような気がするのだが、TypeScriptを使い始めてだいぶ慣れてしまったので、何に困ったのか全てを覚えていない。思い出したら、記事を追記して投稿していこうと思う。こういう場合にどう解決するべきなのかという疑問があれば、それを考えていきたい。
1. JavaScriptからTypeScript移行時に最初に訪れる試練、連想配列
これを一度もやらずにTypeScriptを使っている者がいたとすれば驚愕である。JavaScriptからTypeScriptに移行する場合に、最初に放ってくる地獄からの息吹である。これに心を折られ、踵を返してしまうものもいることだろう。
- 失敗例
プロパティの存在しないオブジェクトが作られ、aが入らない
const value1 = {}
value1.a = 10 //ここでエラー
console.log(value1.a)
- 修正例
連想配列にする
const value1:{[key:string]:any} = {}
value1.a = 10
console.log(value1.a)
2. interfaceを使うほどでもないので、何度も同じ型を定義してしまう
こうして型を付けるのがかったるいと思ってしまうのだ
const value1: { a?: number, b?: number } = { a: 100 }
const value2: { a?: number, b?: number } = { b: 200 }
typeofで型が再利用できる
const value1: { a?: number, b?: number} = { a: 100 }
const value2: typeof value1 = { b: 200 }
console.log(value2.a) //コンパイルが通る
3. イキってstrictをtrueにしたときに訪れる試練、オブジェクトのキー
valuesは a|b|c のキーしか持たないので、string型のkeyを突っ込むとエラーにされる。Object.keysから推論して欲しいところだが、現在はそうなっていない。
const values = {'a':100,'b':200,'c':300}
for(const key of Object.keys(values)){
console.log(`${key}:${values[key]}`) //values[key]でエラー
}
よく見かけるのはアンチパターンの対処法だ。型を吹き飛ばして無理やり引っ張り出す手法だ。テロリストを倒すのに、住民もろとも爆撃してしまうような暴挙と呼べる。この場合はkeyの型を正しく認識させなければいけない。typeofで型を取り出し、さらにkeyofでキーの型を取り出すという二重構造だ。
const value2 = { 'a': 100, 'b': 200, 'c': 300 }
for (const key of Object.keys(value2)) {
console.log(`${key}:${value2[key as keyof typeof value2]}`)
//console.log(`${key}:${(value2 as any)[key]}`) //よくあるアンチパターン
}
※2019/05/23追記
Object.keysの宣言を上書きして適切な型を返すようにすれば、地獄にいるのに清涼感を味わえる
そもそもkeyを返しているのにstringが返る、元の定義が微妙なのではないだろうか?
interface ObjectConstructor {
keys<T>(o: T): (keyof T)[];
}
const value2 = { 'a': 100, 'b': 200, 'c': 300 }
for (const key of Object.keys(value2)) {
console.log(`${key}:${value2[key]}`) //そのまま通る
}
4.1 intarface中のプロパティの型が欲しい場合
interface自体を使えればよいのだが、中のプロパティの型が欲しいケース
巨大な構造だったら再定義は本当の地獄だ
interface AnyData{
values:{
a:number
b:number}[]
}
}
const proc = (values)=>{ //←ここで型を付けないといけない
console.log(`a:${values.a} b:${values.b}`)
}
const anyData: AnyData = { values: [{ a: 100, b: 200 }] }
proc(anyData.values) //インタフェイスの中の一部分を渡す
ちなみにインタフェイス中のプロパティの型を取得する方法は、「もしかしてこれがやりたいんでしょ」とVSCodeが教えてくれた
VSCode先生、一生はついていかないけど適当なところまではついていきます!
interface AnyData {
value: {
a: number
b: number
}
}
//const proc = (values: {a:number,b:number}) => { //再定義してしまう書き方
const proc = (values: AnyData["value"]) => { //元の定義を利用
console.log(`a:${values.a} b:${values.b}`)
}
const anyData: AnyData = { value: { a: 100, b: 200 } }
proc(anyData.value)
4.2 intarface中のプロパティの型が欲しいのに ? が邪魔をしている場合
※ 2019/5/23 追記
以前書いたプログラムの手直しをしていたら、こういうケースに遭遇した
現時点のtsで型の否定(undefinedを除外)をする方法が導入されていない以上、詰んだかと思われたが何とかなった
interface AnyData {
value?: { //?がついていると、aやbの型にたどり着けなくなる
a: number
b: number
}
}
const proc = (value: AnyData["value"]["a"]) => { //aが存在しないと怒られる
console.log(value)
}
オラ、この地獄でワクワクしてきたぞ
interface AnyData {
value?: {
a: number
b: number
}
}
const proc = (value: (AnyData["value"] & {})["a"]) => { //{}を詰め込む
console.log(value)
}
やった者勝ちみたいな世界だ
※ 2019/5/28 追記 recordareさんから
-?を使った書き方
interface AnyData {
value?: {
a: number
b: number
}
}
type IgnoreOptional<T, K extends keyof T=keyof T> = {[P in K]-?: T[P]}[K];
const proc = (value: IgnoreOptional<AnyData["value"]>["a"]) => {
console.log(value)
}
※ 2019/5/28 追記 uhyoさんから
NonNullableで除去可能だった
interface AnyData {
value?: {
a: number
b: number
}
}
const proc = (value: NonNullable<AnyData["value"]>["a"]) => {
console.log(value)
}
4.3 分かれ道に差し掛かった場合
※ 2019/5/28 追記
Extractで分かれ道も条件を指定可能
もう、何でもありか・・・
interface AnyData {
value: {
a: number
b: number
}|{
c:number;
d:number;
}
}
const proc = (value: Extract<AnyData["value"],{"a":any}>["a"]) => {
console.log(value)
}
5. プロパティがあるか確認しようとするとエラーになるケース
これは解説記事が多いので、いまさらかもしれない
//プロパティがあるか確認しようとするとエラー
interface A{
a:number
}
interface B{
b:number
}
const proc2 = (value: A | B)=>{
if(typeof value.a !== 'undefined') //この時点でaが使えない
console.log(value.a)
}
proc2({a:100})
interface A {
a: number
}
interface B {
b: number
}
const proc2 = (value: A | B) => {
if ("a" in value) //TypeScriptではこう書かないといけない
console.log(value.a)
}
proc2({ a: 100 })
6. HTMLElementに追加プロパティを設定したい場合
素のJavaScriptで気軽にやっていたこれは、エラーとなる
const divElement = document.createElement('div')
divElement.numValue = 100 //HTMLDIVElementにnumValueが存在しないのでエラー
よくあるアンチパターンがこれだ
なんでもanyさえあれば解決という使い方
anyは免罪符か何かだろうか?
const divElement = document.createElement('div');
(divElement as any).numValue = 100 //アンチパターン
わざわざinterfaceを作るまでも無く、その場でプロパティを追加したければ以下のように設定する
const divElement: HTMLDivElement & { numValue?: number } = document.createElement('div')
divElement.numValue = 100
7. childNodesでclassNameなどが使えない場合
childNodesの中に入っているのがChildNodeになっている
素のJavaScriptからプログラムを持ってくると、かなりの確率で引っかかるポイントだ
const divElement = document.querySelector('div')
if (divElement) {
const childNodes = divElement.childNodes //これが NodeListOf<ChildNode> になっている
for (let i = 0, length = childNodes.length; i < length; i++) {
const node = childNodes[i]
node.className = 'CustomClass' //これが使えない
}
}
HTMLElementにキャストしてやれば、関連プロパティにアクセスできる
const divElement = document.querySelector('div')
if (divElement) {
const childNodes = divElement.childNodes as NodeListOf<HTMLElement>
for (let i = 0, length = childNodes.length; i < length; i++) {
const node = childNodes[i]
node.className = 'CustomClass'
}
}
きちんとチェックすれば as はいらない
const divElement = document.querySelector('div')
if (divElement) {
const childNodes = divElement.childNodes
for (let i = 0, length = childNodes.length; i < length; i++) {
const node = childNodes[i]
if (node instanceof HTMLElement)
node.className = 'CustomClass'
}
}
地獄から天国への道
TypeScriptの地獄から、天国へいたる道は長く険しい。そして何人もの人間がエンマ大王に舌を抜かれ沈黙する。しかしそこを抜けた先には、必ずやヘブンがあるはずだ。釜茹でにされようが、針で刺されようが、地道に賽の河原で石を積み上げていくしか方法はないのである。
地獄を知らぬ者に、天国を感じることはできないのだから。