92
95

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 5 years have passed since last update.

地獄からの使者、その名はTypeScript ~ 固かった型の形 ~ 2

Last updated at Posted at 2019-05-22

地獄からの使者、その名はTypeScript ~ 固かった型の形 ~

 TypeScriptは便利である。変な書き方をしていれば教えてくれるし、開発環境が入力補完の支援もしてくれる。まさにヘブンである。しかし書き方を知らないと、その世界は地獄と化す。真っ赤なエラーに焼き尽くされた地獄に。

 地獄を回避するためには解決法を知ることである。TypeScriptは今も刻一刻と進化しており、昨日の知識は原始の人間が棒の握り方を覚えたに等しい。

 ということで地獄を渡り、天国へ到達するための知恵の一部を紹介していきたいと思う。たぶん他にもあるような気がするのだが、TypeScriptを使い始めてだいぶ慣れてしまったので、何に困ったのか全てを覚えていない。思い出したら、記事を追記して投稿していこうと思う。こういう場合にどう解決するべきなのかという疑問があれば、それを考えていきたい。

1. JavaScriptからTypeScript移行時に最初に訪れる試練、連想配列

 これを一度もやらずにTypeScriptを使っている者がいたとすれば驚愕である。JavaScriptからTypeScriptに移行する場合に、最初に放ってくる地獄からの息吹である。これに心を折られ、踵を返してしまうものもいることだろう。

  • 失敗例
    プロパティの存在しないオブジェクトが作られ、aが入らない
.ts
const value1 = {}
value1.a = 10 //ここでエラー
console.log(value1.a)
  • 修正例
    連想配列にする
.ts
const value1:{[key:string]:any} = {}
value1.a = 10
console.log(value1.a)

2. interfaceを使うほどでもないので、何度も同じ型を定義してしまう

 こうして型を付けるのがかったるいと思ってしまうのだ

.ts
const value1: { a?: number, b?: number } = { a: 100 }
const value2: { a?: number, b?: number } = { b: 200 }

 typeofで型が再利用できる

.ts
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から推論して欲しいところだが、現在はそうなっていない。

.ts
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でキーの型を取り出すという二重構造だ。

.ts
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が返る、元の定義が微妙なのではないだろうか?

.ts
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自体を使えればよいのだが、中のプロパティの型が欲しいケース
 巨大な構造だったら再定義は本当の地獄だ

.ts
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先生、一生はついていかないけど適当なところまではついていきます!

.ts
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を除外)をする方法が導入されていない以上、詰んだかと思われたが何とかなった

.ts
interface AnyData {
	value?: { //?がついていると、aやbの型にたどり着けなくなる
		a: number
		b: number
	}
}
const proc = (value: AnyData["value"]["a"]) => { //aが存在しないと怒られる
	console.log(value)
}

 オラ、この地獄でワクワクしてきたぞ

.ts
interface AnyData {
	value?: { 
		a: number
		b: number
	}
}
const proc = (value: (AnyData["value"] & {})["a"]) => { //{}を詰め込む
	console.log(value)
}

やった者勝ちみたいな世界だ

※ 2019/5/28 追記 recordareさんから
 -?を使った書き方

.ts
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で除去可能だった

.ts
interface AnyData {
	value?: { 
		a: number
		b: number
	}
}
const proc = (value: NonNullable<AnyData["value"]>["a"]) => {
    console.log(value)
}

4.3 分かれ道に差し掛かった場合

※ 2019/5/28 追記

 Extractで分かれ道も条件を指定可能
 もう、何でもありか・・・

.ts
interface AnyData {
	value: {
		a: number
		b: number

    }|{
         c:number;
         d:number;
    }
}
const proc = (value: Extract<AnyData["value"],{"a":any}>["a"]) => { 
 	console.log(value)
}

5. プロパティがあるか確認しようとするとエラーになるケース

 これは解説記事が多いので、いまさらかもしれない

.ts
//プロパティがあるか確認しようとするとエラー
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})
.ts
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で気軽にやっていたこれは、エラーとなる

.ts
const divElement = document.createElement('div')
divElement.numValue = 100 //HTMLDIVElementにnumValueが存在しないのでエラー

 よくあるアンチパターンがこれだ
 なんでもanyさえあれば解決という使い方
 anyは免罪符か何かだろうか?

.ts
const divElement = document.createElement('div');
(divElement as any).numValue = 100 //アンチパターン

わざわざinterfaceを作るまでも無く、その場でプロパティを追加したければ以下のように設定する

.ts
const divElement: HTMLDivElement & { numValue?: number } = document.createElement('div')
divElement.numValue = 100 

7. childNodesでclassNameなどが使えない場合

 childNodesの中に入っているのがChildNodeになっている
 素のJavaScriptからプログラムを持ってくると、かなりの確率で引っかかるポイントだ

.ts
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にキャストしてやれば、関連プロパティにアクセスできる

.ts
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 はいらない

.ts
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の地獄から、天国へいたる道は長く険しい。そして何人もの人間がエンマ大王に舌を抜かれ沈黙する。しかしそこを抜けた先には、必ずやヘブンがあるはずだ。釜茹でにされようが、針で刺されようが、地道に賽の河原で石を積み上げていくしか方法はないのである。

 地獄を知らぬ者に、天国を感じることはできないのだから。

92
95
4

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
92
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?