かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript 配列から Falsy な値をフィルタリングしたい…したかった…

JavaScript で配列から Falsy な値をフィルターする際に Array.filter(Boolean) をよく使っていたのですが TypeScript だと型がうまく推論されなかったので TypeScript で配列から Falsy な値を取り除く方法を調べてたメモ

結論

思った以上に沼だった…

  1. TypeScript には is はあるが、〇〇以外 NOR を表すものがない
  2. TypeScript には現状 (v4.9.3 現在) NaN を表す型が無いので .filter(Boolean) の結果を型情報にすることはできなかった(NaN はあくまで number 型)
  3. 暗黙的な Falsy は 0 が削除されてしまったり逆にバグの温床になるので、Flasy を除去しなけれなならない状況なのは設計を見直したほうが良いのではないか?
  4. NonNullablenullundefined を除去する程度に使うのが良さそう

nullundefined を除去する Filter

const nonNullableFilter = 
  <T>(value: T): value is NonNullable<T> => (value !== null && value !== undefined);

array.filter(nonNullableFilter);

JavaScript の Falsy な値

  • false … false キーワード
  • 0 ... 数値ゼロ
  • -0 ... 数値マイナスゼロ
  • 0n … BigInt で、論理値として使用された場合、 Number と同じ規則に従います。 0n は偽値です。
  • "" … 空文字列の値
  • nullnull - 何も値が存在しないこと
  • undefinedundefined - プリミティブ値
  • NaNNaN - 非数

cf. Falsy (偽値) - MDN Web Docs 用語集&colon; ウェブ関連用語の定義 | MDN

Falsy な値は Boolean()false になるので、.filter(Boolean) で除外できる

[0, -0, 0n, false, null, undefined, NaN, ""].map((item) => Boolean(item))
// => [false, false, false, false, false, false, false, false]

[0, -0, 0n, false, null, undefined, NaN, ""].filter(Boolean)
// => []

TypeScript で Array.filter(Boolean)型推論がうまく行かない

const arr = [0, -0, 0n, false, null, undefined, NaN, ""];
const newArr = arr.filter(Boolean);
// => const newArr: (string | number | bigint | boolean | null | undefined)[]

.filter(Boolean) をしても元の配列の型のまま推論されてしまう

1. 型情報から nullundefined を除く

Falsy な値は型が複雑になりそうなので、まずは nullable な型情報を除去してみる。NonNullable を使うと nullable (nullundefined) を除去できる

const arr = [0, -0, 0n, false, null, undefined, NaN, ""];
const newArr = arr.filter((item): item is NonNullable<typeof item> => 
  (item !== null && item !== undefined));
// newArr: NonNullable<string | number | bigint | boolean | null | undefined>[]
// => [0, -0, 0n, false, NaN, '', 1, '2']

コールバック関数をユーティリティ化する

毎回 (item): item is NonNullable<typeof item> => (item !== null && item !== undefined) を書くのは大変なので、ユーティリティ化しておくと便利

const nonNullableFilter = <T>(value: T): value is NonNullable<T> => (value !== null && value !== undefined);

👇 ユーティリティに置換

const arr = [0, -0, 0n, false, null, undefined, NaN, ""];
const newArr = arr.filter(nonNullableFilter);
// newArr: NonNullable<string | number | bigint | boolean | null | undefined>[]
// => [0, -0, 0n, false, NaN, '', 1, '2']

📝 null 合体演算子 (??) を使う方法

Null 合体演算子 (??) は論理演算子の一種です。この演算子は左辺が null または undefined の場合に右の値を返し、それ以外の場合に左の値を返します。
cf. Null 合体演算子 (??) - JavaScript | MDN

const arr = [0, -0, 0n, false, null, undefined, NaN, ""];

// filter のコールバックは boolean 型でないとダメなのでそのままエラーになる
arr.filter((item): item is NonNullable<typeof item> => 
  (item ?? false));
// Type 'string | number | bigint | boolean' is not assignable to type 'boolean'.
//  Type 'string' is not assignable to type 'boolean'.


const newArr = arr.filter((item): item is NonNullable<typeof item> => 
  !!(item ?? false));
// newArr: NonNullable<string | number | bigint | boolean | null | undefined>[]

ただ値によっては false ?? false になっていたりで少し気持ち悪い。個人的に明示的に (item !== null && item !== undefined) としておいた方が見通しが良いのでは?という気もする

2. 型情報からも Falsy な値を除去したい

⚠ NaN は上手く除去できない

NaN は number 型でしかなく型として定義できない

type Falsy = 0 | -0 | 0n | false | null | undefined | '' | NaN;

=> 'NaN' refers to a value, but is being used as a type here. Did you mean 'typeof NaN'?

  • NaN をプリミティブ型として定義することはできない
    • NaN == NaNfalse なのでプリミティブ型にできても、そもそも判定できない
    • NaN かどうかは isNaN(number) でしか判定できない
  • 型エラーにならないようにtypeof NaN とすると、typeof NaN = number なので number型 が Falsy に含まれてしまう事になるので良くない

2-1. .filter(Boolean) を型定義のある関数に置き換える

  • .filter(Boolean).filter((item) => Boolean(item)) の省略形
  • コールバック関数を型定義のあるものに置き換える
type Falsy = 0 | -0 | 0n | false | null | undefined | '';
const isTruthy = <T>(x: T | Falsy): x is T => !!x;
const isTruthy = <T>(x: T | Falsy): x is T => !!x; の解説
  1. 引数 x の型が T または Falsy
  2. !!xtrue なら x is T (Falsy ではない)

2-2. 渡す配列を as const しなければ正しく判定できない

type Falsy = 0 | -0 | 0n | false | null | undefined | '';
const isTruthy = <T>(x: T | Falsy): x is T => !!x;

const result = [0, -0, 0n, false, null, undefined, '', 1, '2'].filter(isTruthy);
// => result: (string | number | bigint | true)[]

const result = ([0, -0, 0n, false, null, undefined, '', 1, '2'] as const).filter(isTruthy);
// => result: (1 | "2")[]

渡す配列を as const してないと isTruthyx: T | Falsy の部分で number や bigint や boolean という型情報として判定されるので T 扱いになり、00nfalse が除去されているか判別されない。
as const する事でプリミティブな値として渡されるので 00nfalse という値として渡されるので Falsy型 の値だと判定されるという仕組み。

NaN があると as const しても number 型が帰る

type Falsy = 0 | -0 | 0n | false | null | undefined | '';
const isTruthy = <T>(x: T | Falsy): x is T => !!x;

const result = ([0, -0, 0n, false, null, undefined, '', 1, '2', NaN] as const).filter(isTruthy);
// => result: (number | "2")[]

const result = ([0, -0, 0n, false, null, undefined, '', 1, '2', NaN] as const).filter(isTruthy);
// => result: (number | "2")[]

const result = ([NaN] as const).filter(isTruthy);
// => result: number[]

所管

Array.filyer(Boolean) で Falsy な値を除去した時に型情報も合わせたかっただけなのですが思った以上に沼だった。

結果的に今の実力では NaN も除去した型を作ることができなかった。現実的には TypeScript で暗黙的な型変換の Falsy で何かを判定することほぼ無いと思うので NonNullablenullundefined を取り除けく方法で十分な気がしている。(むしろ暗黙的な Falsy で判定すると意図的な 0 が除去されてしまうとかバグの原因だと思っている)

おわり


[参考]

Vite index.html を移動させて開発ディレクトリを作りたいときの Tips

Vite Vanilla JS で静的サイトを作るテンプレートを作成していて index.html を移動させると色々大変だったのでメモを残しておく
作ったもの

デフォルトの Vite プロジェクト

$ npm create vite@latest
✔ Project name: … <Project Name>
✔ Select a framework: › Vanilla
✔ Select a variant: › TypeScript

👇 構成

/root
  |- /public
  |- /src
  |    |- main.ts
  |- index.html
  |- tsconfig.json

ゴール

/root
  |- /project
  |     |- /public
  |     |- /src
  |     |    |- main.ts
  |     |- index.html
  |- tsconfig.json
  • build した dist ディレクトリは root 直下に出力される
  • .env ファイルは root 直下に置きたい

1. vite.config.js をプロジェクトのルートに作成する

$ touch vite.config.js
/root
  |- /public
  |- /src
  |    |- main.ts
  |- index.html
  |- vite.config.js ←
  |- tsconfig.json

2. 開発環境の root を project ディレクトリにする

/root
  |- /project ← 開発用のディレクトリ
  |     |- /public
  |     |- /src
  |     |    |- main.ts
  |     |- index.html
  |- vite.config.js
  |- tsconfig.json
  1. project ディレクトリを作成しファイルを移動させる
  2. vite.config.js を作成しパスの変更の設定を行う

vite.config.js

import { defineConfig } from 'vite';

export default defineConfig({
  // index.html の場所
  root: 'project',
  // アセットなどのパスを変換するベースとなるパス
  // `/foo/` とすると `/foo/` 始まりのパスに変換される
  base: '/',
  // 静的ファイルの場所
  //  `public` を指定した場合 `<root>/public` が静的ファイルの格納場所になる
  publicDir: 'public',
});

cf. Shared Options | Vite

3. ビルドしたファイルの出力先をプロジェクトのルートにする

デフォルトだと vite.config.js の root の場所に出力される

/root
  |- /dist ← ここに出力されるようにしたい
  |- /project

build.outDir オプションでビルドファイルの出力場所を指定できる

vite.config.js

import { defineConfig } from 'vite';

export default defineConfig({
  root: 'project',
  base: '/',
  publicDir: 'public',
  build: {
    // `root` からの相対パスで指定する
    outDir: '../dist',
  },
});

build.outDir

  • Type: string
  • Default: dist

Specify the output directory (relative to project root).
cf. Build Options | Vite

4. .env ファイルの場所をプロジェクトのルートにする

/root
  |- /project
  |- .env ← ここに `.env` を置きたい
  |- vite.config.js
  |- tsconfig.json

Vite はデフォルトで .env を読み込める。デフォルトでは .env の場所は vite.config.js の root の場所なので /project ディレクトリとなる。envDir を使って .env のファイルの場所を指定できるのでプロジェクトのルートに変更する

vite.config.js

import { defineConfig } from 'vite';

export default defineConfig({
  root: 'project',
  base: '/',
  publicDir: 'public',
    build: {
    outDir: '../dist',
  },
  // 絶対パスか、`root` からの相対パスで指定する
  envDir: '../',
});

envDir
The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root.
cf. Shared Options | Vite

絶対パスの場合は __dirname を使う。パスの最後が / で終わる必要があるので path.join(__dirname, '/') と指定すればプロジェクトのルートになる

5. path alias の解決

vite-tsconfig-paths を使ってパスエイリアスの設定を行う場合

  1. vite-tsconfig-paths から tsconfig.json の場所を指定する
  2. tsconfig.json でパスエイリアス/project からの指定で行う

1. vite-tsconfig-paths から tsconfig.json の場所を指定する

vite.config.js

import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  root: 'project',
  base: '/',
  publicDir: 'public',
  build: {
    outDir: '../dist',
  },
  envDir: '../',
  plugins: [
    // `root` からの相対パスで `tsconfig.json` の場所を指定する
    tsconfigPaths({
      root: '../',
    }),
  ],
});

Options
root: string
The directory to search for tsconfig.json files.
The default value of this option depends on whether projects is defined. If it is, then the Vite project root is used. Otherwise, Vite's searchForWorkspaceRoot function is used.

cf. - vite-tsconfig-paths - npm - Vite + React で path alias を使いたい! - かもメモ

2. tsconfig.json でパスエイリアス/project からの指定で行う

tsconfig.json

{
  "compilerOptions": {
    // …
    "baseUrl": "./project",
    "paths": {
      "@/*": ["./src/*"],
      "~/*": ["./public/*"]
    },
  },
}

cf. - TypeScript: TSConfig Reference - Docs on every TSConfig option - tsconfig.jsonのrootDirとbaseUrlに関するメモ [TypeScript] - Qiita - TypeScript: TSConfig Reference - Docs on every TSConfig option - TypeScript: Documentation - Module Resolution - ts-nodeを使ってtsconfigのpathsをちゃんと読み込ませる | Oinari Tech Blog

3. VS Code のパスエイリアスの補完が効くようにする

./vscode/settings.json"path-autocomplete.pathMappings" で指定する

{
  // …
  "path-autocomplete.pathMappings": {
    "@": "${folder}/project/src",
    "~": "${folder}/project/public"
  }
}

これで Vite のプロジェクトで index.html を移動させて開発関連のファイルを丸っと単一のディレクトリに格納することができました!
おわり ₍ ᐢ. ̫ .ᐢ ₎


[参考]

サムネ画像のアイディアネタ切れ

Vite TypeScript __direname がエラーになる

Vite + TypeScript + ESLint で作った環境で Multi-Page App の設定をしようとした際に __direname がエラーになってしまったのでメモ

TypeScript Cannot find name '__dirname'

Cannot find name '__dirname' のエラーが出る場合は Node の型が無いので @types/node をインストールする

$ npm i -D @types/node

tsconfig.json"types": ["node"] の設定を追加する

{
  "compilerOptions": {
+   "types": ["node"], 

ESLint '__dirname' is not defined.eslint (no-undef)

ESLint が node の書き方を許容していないのが原因
.eslintrc.cjs の env に node: true を追加すればOK

module.exports = {
  env: {
    browser: true,
    es2021: true,
+   node: true,
  },

設定系はやる度になんかハマる…
おわり。


[参考]

ぼっち・ざ・ろっく をみましょう