LoginSignup
0
0

TypeScript は forEach での変数の更新を認識できないようだ

Posted at

コード例

例として、「x 座標が正である点の中で、x 座標が最小の点」を求めるプログラムを書いてみた。

interface Point {
    x: number;
    y: number;
}

const points: Point[] = [
    {x: 7, y: 5},
    {x: -1, y: 20},
    {x: 3, y: 10},
];

let selectedPoint : Point | null = null;

points.forEach((point) => {
    if (point.x > 0 && (!selectedPoint || selectedPoint.x > point.x)) {
        selectedPoint = point;
    }
});

if (selectedPoint) {
    console.log(selectedPoint.x);
}

このコードを Playground (v5.3.3) に入力すると、以下のエラーが出てしまった。

Property 'x' does not exist on type 'never'.

このエラーは、以下の行で発生している。

    console.log(selectedPoint.x);

forEach 内で selectedPoint を更新する可能性があるにもかかわらず、上記の行の前の行

if (selectedPoint) {

において、selectedPoint の型がなぜか null になってしまっており、この if 文の中では never になってしまっている。

このコードを実行してみると、

[LOG]: 3 

と出力され、正しく求まっていることがわかる。

回避法

この章の地の文には、個人の感想が含まれる。

普通の for 文を使う

forEach なんていうオシャレな記法はやめて、普通の for 文で普通に実装する。
先ほどのコードからの変更が最小限であり、素直で良い書き方である。

interface Point {
    x: number;
    y: number;
}

const points: Point[] = [
    {x: 7, y: 5},
    {x: -1, y: 20},
    {x: 3, y: 10},
];

let selectedPoint : Point | null = null;

for (let i = 0; i < points.length; i++) {
    const point = points[i];
    if (point.x > 0 && (!selectedPoint || selectedPoint.x > point.x)) {
        selectedPoint = point;
    }
}

if (selectedPoint) {
    console.log(selectedPoint.x);
}

reduce を使う

ループ内で値を更新していくということで、reduce を用いる。
オシャレさは維持できるが、初期値が後ろに行くことでわかりやすさが下がる可能性がある。

interface Point {
    x: number;
    y: number;
}

const points: Point[] = [
    {x: 7, y: 5},
    {x: -1, y: 20},
    {x: 3, y: 10},
];

const selectedPoint = points.reduce((selectedPoint: Point | null, point) => {
    if (point.x > 0 && (!selectedPoint || selectedPoint.x > point.x)) {
        selectedPoint = point;
    }
    return selectedPoint;
}, null);

if (selectedPoint) {
    console.log(selectedPoint.x);
}

アルゴリズムを切り替える

「x 座標が正である点の中で、x 座標が最小の点を求める」という処理を、

  1. x 座標が正である点を求める
  2. その中で x 座標が最小の点を求める

という処理に分割する。
オシャレさはさらに上がる。
このような分割が可能な特殊な場合向け。

interface Point {
    x: number;
    y: number;
}

const points: Point[] = [
    {x: 7, y: 5},
    {x: -1, y: 20},
    {x: 3, y: 10},
];

const positivePoints = points.filter((point) => point.x > 0);
const selectedPoint = positivePoints.length > 0
    ? positivePoints.reduce((acc, point) => point.x < acc.x ? point : acc)
    : null;

if (selectedPoint) {
    console.log(selectedPoint.x);
}

as でごまかす

as強制的に型を指定し、エラーを握りつぶす
考えたくない人向け。

interface Point {
    x: number;
    y: number;
}

const points: Point[] = [
    {x: 7, y: 5},
    {x: -1, y: 20},
    {x: 3, y: 10},
];

let selectedPoint : Point | null = null;

points.forEach((point) => {
    if (point.x > 0 && (!selectedPoint || selectedPoint.x > point.x)) {
        selectedPoint = point;
    }
});

if (selectedPoint) {
    console.log((selectedPoint as Point).x);
}

まとめ

以下の条件を満たす書き方をするのがいいだろう。

  • as などの強行突破をなるべくしない
  • まわりから怒られない (コーディング規約に違反しないなど)
  • 自分がわかりやすい・書きやすい
0
0
0

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
0
0