iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

まだconstアサーション使ってないの?

□はじめに

こんにちは!株式会社iimonでフロントエンドエンジニアを担当している「みよちゃん」です!

自分は普段の業務でTypeScriptを使用して自社プロダクトの機能追加やデバッグを行っております。新機能の追加や機能の拡張を実装するうえで、「変更する必要のない値は変更できなくしておく」ことが重要であると感じる今日この頃でございます。今回はconstアサーションを使用するうえで少し戸惑ったことがあり、constアサーションについて調べなおしました。その内容を以下にまとめていきたいと思います。

□constアサーション

◇そもそもconstアサーションって?

TypeScriptにおけるconstアサーションは、変数の型推論をより厳密にする機能です。通常、TypeScrptは変数に代入された値から型を推論しますが、constアサーションを使用すると、その値をリテラル型として扱い、変更が不可能な最も厳密な型を付与することができます。

リテラル

リテラル型に関しても念のため簡単に説明しておきます。

TypeScriptにおけるリテラル型はプリミティブ型の特定の値だけを代入可能にする型を表現します。

具体的な例を見てみましょう!

通常letで宣言したnumber型の変数はnumber型の値であれば自由に再代入できます。

let num:number = 1;
num = 100;

しかしリテラル型の変数は特定の値以外を代入することはできません。

let limitedNum:1 = 1;
limitedNum = 100;
// => Type '100' is not assignable to type '1'.

リテラル型は一般的にマジックナンバーやステートの表現など、値が変更されないものに用いられます。

◇書き換え可能なObject

例えば、iimon社員全員を年代コードを使用してグループ分けすると仮定しましょう。

年代コードをオブジェクトとして保持するとします。この値はどの場所でも共通して使用すると考慮しているため、書き換えられることは想定していません。

const GENERATION_CODE_OBJ = {
    TEENS:1,
    TWENTIES:2,
    THIRTIES:3,
    FORTIES:4,
    OTHERS:5
}

しかし、この値は容易に書き換えることができます。

getGenerationCode(age:number){
    const codeObj = GENERATION_CODE_OBJ;
    codeObj.TWENTIES= 4
    if(age < 20){
        return codeObj.TEENS;
    }
    if(age < 30){
        return codeObj.TWENTIES;
    }
    if(age < 40){
        return codeObj.THRTIES;
    }
    if(age < 50){
        return codeObj.FORTIES;
    }
    return OTHERS
}

流石にこんな訳わからん変更のされ方はしないと思いますが、importされている箇所が増えるとどこかで書き換わってしまうこともあり得ます。

const iimonkun = {
    name: 'iimonKun',
    age: 22,
}
const generationCode = getGenerationCode(iimonkun.age);
// => generationCode: 4 

残念ですが、22歳の若手社員いいもん君はフレッシュなのにも関わらず、今後40代の中堅社員として扱われてしまいます。

◇constアサーションを使用する

constアサーションを使用すると値を書き換えることができなくなります。

const GENERATION_CODE_OBJ = {
    TEENS:1,
    TWENTIES:2,
    THIRTIES:3,
    FORTIES:4,
    OTHERS:5
} as const

それではうっかり書き換えようとしてみましょう!

getGenerationCode(age:number){
    const codeObj = GENERATION_CODE_OBJ;
    codeObj.TWENTIES= 4
    ~~~~~~~~~~~~~~~~~~
    // Cannot assign to 'TWENTIES' because it is a read-only property
    if(age < 20){
        return codeObj.TEENS;
    }
    if(age < 30){
        return codeObj.TWENTIES;
    }
    if(age < 40){
        return codeObj.THRTIES;
    }
    if(age < 50){
        return codeObj.FORTIES;
    }
    return OTHERS
}

TWENTIESは読み取り専用のため代入できません」と怒られてしまい、再代入することはできません。これで書き換えられたくない値をついうっかり書き換えてしまうことはなくなりますね!

無事いいもん君は今後もフレッシュな若手として扱ってもらえそうです!!

◇readonlyとの違い

ここまで読んでいただいた方の中には、「constアサーションについては分かったけど、readonlyを使用すればよいのでは?」と思われた方もいるかと思います。この二つはどちらもオブジェクトのプロパティーreadonlyにする機能は同じですが、2点ほど異なる要素があります。

◆readonly

  1. readonlyはプロパティーごとに適用することができる
  2. プロパティー内にオブジェクトがある場合、中の階層までは対象にならない

◆constアサーション

  1. すべてのプロパティーが対象となる
  2. 再帰的にreadonlyにできる

ここではreadonlyconstアサーションの違いを、チームオブジェクトを使用してみてみましょう。

チームオブジェクトはteamName teamColor membersCountの3つのプロパティーを持っており、うち二つはreadonlyとなっています。

◆readonlyを使用した場合

type MembersCountType = {
    front:number,
    server:number,
    inflastructure:number
}

type TeamType = {
    readonly teamName:string,
    teamColor:string,
    readonly membersCount:MembersCountType,
}

const redTeam:TeamType= {
    teamName:'teamRed',
    teamColor:'red',
    membersCount{
        front:5,
        server:2,
        inflastructure:1,
    }    
}

redTeam.teamName = 'teamBlue'; 
// => Cannot assign to 'teamName' because it is a read-only property
redTeam.teamColor = 'blue';
redTeam.membersCount.front = 6;

最後の値の再代入部分を見ていただくとteamNameteamColorは想像通りの挙動かと思います。しかし、membersCount内のプロパティーには値を再代入できています。

これはreadonlyをつけたプロパティがオブジェクトである場合に、そのオブジェクトのプロパティまでreadonlyにはしないことに起因します。

◆constアサーションの場合

同様の実装をconstアサーションを使用してやってみます!

const redTeam = {
    teamName:'teamRed',
    teamColor:'red',
    membersCount{
        front:5,
        server:2,
        inflastructure:1,
    }    
} as const

redTeam.teamName = 'teamBlue'; 
// => Cannot assign to 'teamName' because it is a read-only property
redTeam.teamColor = 'blue';
// => Cannot assign to 'teamColor' because it is a read-only property
redTeam.membersCount.front = 6;
// => Cannot assign to 'front' because it is a read-only property

◇constアサーションの罠

ここまでconstアサーションの使用例を通して、その安全性を理解していただけたかと思います。そんなconstアサーションですが、自分が最近躓いた内容に関して紹介したいと思います。

◆型が強固すぎる。。

オブジェクトの中からいくつかのプロパティーを抽出してリストを作り、動的な値がその中に含まれるかを確認するとします。

今回の例はテストの点数により評価が決まり、特定の評価だった場合は合格した旨を出力します(点数で判定すればよいのかというツッコミは心の中にとどめておいていただきたいです)

TEST_GRADEはすべての教科のテストで共通のため、書き換えられないようにconstアサーションしておきます。

const TEST_GRADES = {
    PERFECT:1,
    GREAT:2,
    GOOD:3,
    NOT_GOOD:4,
    BAD:5
} as const

const getRandomGrade = () => {
    const randomNum = Math.random() * 100
    if(randomNum === 100) return TEST_GRADES.PERFECT
    if(randomNum > 80) return TEST_GRADES.GREAT
    if(randomNum > 60) return TEST_GRADES.GOOD
    if(randomNum > 40) return TEST_GRADES.NOT_GOOD
    return TEST_GRADES.BAD
}

const grade = getRandomGrade()
const passList = [TEST_GRADES.PERFECT, TEST_GRADES.GREAT, TEST_GRADES.GOOD]
if(passList.includes(grade)) {
// => Argument of type '1 | 2 | 3 | 4 | 5' is not assignable to parameter of type '1 | 2 | 3'.Type '4' is not assignable to type '1 | 2 | 3'.
    console.log('You passed the test')
};

最後のif文でエラーが出ています。内容は意訳すると「1,2,3,4,5の値は型1|2|3には代入できないよ」といった内容です。

リテラル型の値からなる配列は要素の値から型推論を行うため、passListの型は1|2|3 と推論されています。そのため、4,5が渡される可能性があるためエラーとなっています。

解決方法

  1. passLstの型を指定する

     const passList:number[] = [
         TEST_GRADES.PERFECT, 
         TEST_GRADES.GREAT, 
         TEST_GRADES.GOOD
     ]
     if(passList.includes(grade)) {
         console.log('You passed the test');
     }
    

    passListの型を明示的にnumber[]としておけば、エラーは回避することができます。一番シンプルで簡単ですが、これは型安全性を損なうためあまり良い方法とは言えません。

  2. 型キャストをする

     const passList = [
         TEST_GRADES.PERFECT, 
         TEST_GRADES.GREAT, 
         TEST_GRADES.GOOD
     ] as const
     if(passList.includes(grade as typeof passList[number])) {
         console.log('You passed the test');
     }
    

    型キャストをしてgradeをpassListの要素と一致する値として扱うことでエラーを回避することができます。

  3. includesをあきらめる

     if(
         grade === TEST_GREADES.PERFECT||
         grade === TEST_GRADES.GREAT||
         grade === TEST_GRADES.GOOD
     ) {
         console.log('You passed the test');
     }
    

    より明示的にチェックすることができるしエラーも回避できる。解決方法とは違う気もするが無難。

型ガードを使用するなど、解決策はほかにもありますが今回は分かりやすい3つを紹介しました。

□おわりに

変更する必要のない値・変更したくない値は変更できなくしておくのが最も安全です。その分制約が生まれて不便なこともありますが、想定外に値が変更されることがないのでとっても安心ですね!皆さんも適切にconstアサーションを使用して安全なTypeScriptライフを!!

この記事が少しでもためになった、もしくは面白かったと感じていいただけた方!弊社では現在エンジニアを募集しています!是非一度カジュアルにお話ししましょう! ご応募心よりお待ちしております!!! Wantedly / Green

□参考

https://typescriptbook.jp/reference/values-types-variables/const-assertion

https://typescriptbook.jp/reference/values-types-variables/literal-types

https://zenn.dev/estra/articles/typescript-widening