Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

SoraKumo
TypeScriptでフロントエンドフレームワーク JWF(JavaScript-Window-Framework)を開発しています 世の中のWebシステムをSPA化するため、活動を続けています
https://ttis.croud.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした