> [!attention]
> [[TypeScript 5.5]]からは[[型述語 (TypeScript)|型述語]]が推論可能になったため、本対応は不要。
## 事象
以下のコードで`numbers`に[[Equality narrowing]]が効かない。`numbers: number[]`になってほしいのだが。。
```ts
declare const numberOrNulls: (number | null)[]
const numbers = numberOrNulls.filter(x => x != null)
// ^? const numbers: (number | null)[]
```
## 原因
`filter`に指定された関数([[アロー関数 (JavaScript)|アロー関数]])に[[型述語 (TypeScript)|型述語]]が指定されていないから。
### 詳細
事象のコードで`filter`に指定された引数は `x => x != null` であり、`x != null`は`boolean`型を返す。つまり
```ts
numberOrNulls.filter(x => x != null)
```
は
```ts
numberOrNulls.filter((x: number | null): boolean => x != null)
```
と推論される。
> [!question]- なぜ[[アロー関数 (JavaScript)|アロー関数]]の戻り値は[[型述語 (TypeScript)|型述語]]にならないのか?
>
> [[Equality narrowing]]の[[不等価演算子 (JavaScript)|不等価演算子]]パターンから、直感的に `x != null`の結果は[[Narrowing]]されると思うかもしれない。だが[[Narrowing]]は、[[型ガード (TypeScript)|型ガード]]によって発生するものであり、関数や[[メソッド (TypeScript)|メソッド]]の場合は[[戻り値型]]が明示的に[[型述語 (TypeScript)|型述語]]となっていない限り反映されない。
ここで、`lib.es5.d.ts`にある`filter`メソッドの定義を確認すると、以下2つのシグニチャが定義されている。
```ts
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
```
1つ目の `predicate` は[[戻り値型]]が `value is S` となっており、これは[[型述語 (TypeScript)|型述語]]である。2つ目の `predicates` は戻り値が `unknown` となっている。
`boolean`型が自動的に[[型述語 (TypeScript)|型述語]]へ推論されることはないため1つ目のシグニチャには一致しない。一方、[[unknown型にはすべての型の値を代入できる]]ため、2つ目のシグニチャには一致する。よって以下の定義が利用される。
```ts
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
```
ここで、この`filter`関数の[[戻り値型]]は`T[]`であり、[[型引数 (TypeScript)|型引数]]`T`は[[関数型の式 (TypeScript)|関数型の式]]`predicate`の第1引数と同じ型である。つまり、`T = number | null` ならば[[戻り値型]] `T[]`は `T[] = (number | null)[]` となってしまう。
## 解決方法
[[アロー関数 (JavaScript)|アロー関数]]の[[戻り値型]]に[[型述語 (TypeScript)|型述語]]を指定する。
```ts
declare const numberOrNulls: (number | null)[]
const numbers = numberOrNulls.filter((x): x is number => x != null)
// ^? const numbers: number[]
```
<button class="playground"><a href="https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEYD2A7AzgF3igrgWwCMQYB5GAORwgjQC54AKXQ4+AH2yogEoBtAXQBQg5OizMiMNPAC8nFqQpc0AOgBmASwgZiDBgA9u9ffA3SJrGQD54JgIRzc1boID0r+J88A9APxA">Playground</a></button>
これで、`lib.es5.d.ts`にある以下の`filter`メソッドが呼び出される。
```ts
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
```
`S`が`number`、`T`が`number | null`であり、`filter`も[[戻り値型]]は`S[]`なので`number[]`となる。これで期待通り。
> [!hint]- `<S extends T>` について
>
> 型Sに対する[[ジェネリック制約 (TypeScript)|ジェネリック制約]]として型Tが課されれている。`filter`の場合、引数の型には[[Nullish (JavaScript)|Nullish]]なものが混じっており、[[戻り値型]]には[[Nullish (JavaScript)|Nullish]]なものが含まれていないのが通常であるためこのような定義になっている。
>
> 先ほどの具体的なコードの場合、`filter<S extends number | null>(predicate: (value: number | null, ...): S[]` となるが、`S`は`number | null`に代入可能な型であるため、 `number`、`null`、`number | null`のいずれかとなる。よって、`predicate`の[[型述語 (TypeScript)|型述語]]に`String`や`number | undefined`のような型を指定することはできない。(エラーになる)
ただ、このパターンは頻出するので、[[Nullish (JavaScript)|Nullish]]かどうかを確認する関数を定義し、[[型述語 (TypeScript)|型述語]]を追加したほうがいい。
```ts
function isPresent<T>(value: T | undefined | null): value is T {
return value != null
}
declare const numberOrNulls: (number | null)[]
const numbers = numberOrNulls.filter(isPresent)
// ^? const numbers: number[]
```
<button class="playground"><a href="https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABDAzgBQE4FMVbFAHgBUA+ACgDcBDAGxCwC5EjEAfRcAEy2BjC05tEYEDRoBKJtTpZkKZogDeAKESJsUEBiTT6iAIQBeYaJrKAvsuXcINKtkQQEKKCYC2AIywYA8hgBypihMZCKe3kIiYuIA2gC6Vk5gLu5eGPLGYWl+gWIoAHS8NFDeZKiYOHhQ4soA9LVqjQB6APxAA">Playground</a></button>
### よく間違えるNGなケース
以下の書き方では[[Narrowing]]されない。`isPresent`は[[型述語 (TypeScript)|型述語]]に対応しているが、`(x) => isPresent(x)`は一段階呼び出しが増えており、`isPresent`の[[戻り値型]]は`boolean`であるため、`(x) => isPresent(x)`も`boolean`型を返すから。
```ts
function isPresent<T>(value: T | undefined | null): value is T {
return value != null
}
declare const numberOrNulls: (number | null)[]
const numbers = numberOrNulls.filter((x) => isPresent(x))
// ^? const numbers: (number | null)[]
```
以下の書き方なら期待通りとなるが、都度[[型述語 (TypeScript)|型述語]]を書くのは冗長であり、逆に可読性が落ちてしまう。
```ts
const numbers = numberOrNulls.filter((x): x is number => isPresent(x))
// ^? const numbers: number[]
```
これなら以下の書き方をした方がいい。
```ts
const numbers = numberOrNulls.filter((x): x is number => x != null)
// ^? const numbers: number[]
```
または[[型アサーション (TypeScript)|型アサーション]]を使う。
```ts
const numbers = numberOrNulls.filter((x) => x != null) as number[]
// ^? const numbers: number[]
```
> [!attention]
> 上記の方法はいずれも強引な手法なので `isPresent` を定義して使う方がいいはず。