TypeScriptでプロパティ代入が「is not assignable to 'never'」となるエラーの対策

2021/11/10に公開

はじめに

まずサンプルコードですが、対象のオブジェクトを、別のオブジェクトでプロパティ上書きするような感じの処理になります。

// 仮の型定義、宣言
interface Note {
  id: number,
  label: string,
  value: number
}
declare var noteList: Note[]

function updateNote(param: Note) {
  // note取得(簡易のため、必ず成功するものとする)
  const targetNote = noteList.find((note)=> note.id === param.id)!;

  // 更新
  const propKeys = Object.keys(param) as (keyof Note)[]
  propKeys.forEach((key) => {
    // idは更新しない
    if (key === "id") return

    // ※
    targetNote[key] = param[key]
}

一見すると問題ない(そして動作上も問題ない)ですが、strictルール下では※のプロパティ(Index Signature)代入のところで、

Type 'string | number' is not assignable to type 'never'.

なるエラーが発生します。

strictルールを解いたり@ts-ignoreでの無視も可能なものの、やはり気持ちが悪いので解決していこうと思います。

原因と対策

プロパティ値のparam[key]の型はそのままだとstring|numberになりますが、この型に厳密に適合するプロパティがNoteに存在しないのでエラーになるようです。

しかしif-else文やswitch文でキーごとに場合分けをするとparam[key]の型がstringあるいはnumberいずれかに確定するため、エラーを解消できます。

    // idは更新しない
    if (key === "id") return

    // NG: neverエラー
    targetNote[key] = param[key]

    // OK:if-elseで場合分け
    if (key === "value") {
      // param[key]がnumber型に確定
      targetNote[key] = param[key]
    } else {
      // 同じくstring型に確定
      targetNote[key] = param[key]
    }

    // OK:switch文で場合分け
    switch (key) {
      case "value":
        targetNote[key] = param[key]
        break
      case "label":
        targetNote[key] = param[key]
        break
    }

またお勧めはしませんが、以下のようにアサーションで強引に型の不適合を解消することもできます。
(キー数が多くて場合分けが面倒なときは有効?)

    // "value"以外でもOK(この例では"label"や"id"など、存在するキー名であれば)
    targetNote[key as "value"] = param[key] as any

TS Playgroundで確認

その他

  • あくまで代入すべき値の型が適合しないことがNGのようなので、例えばlabelプロパティ値の型がnumberだった場合、(どのプロパティもnumber型は代入可能なので)エラーは発生しません
    • この点から「neverになるからNGだよ!」とエラーを出すのは微妙に不親切というか混乱のもとという気も…
    • Noteインターフェースには"label"|"value"というunion型に対応するプロパティキーが存在しないのでneverになる、と解釈も可能だけども...?

Discussion