このエントリーをはてなブックマークに追加

はじめに

こんにちは。フロントエンドの開発を担当しています、松岡です。

コードレビューをしている時 x === undefined で比較してガードするケースをよく見かけます。

function something(x) {
  if (x === undefined) {
    return;
  }
  // 何らかの処理
  // ...
}

xnull だった場合に意図しない結果を生み出すためこのような比較はしないで欲しいですとコメントすると、たいてい「どうして?この関数に null が渡される事はないのに?」と不思議がられます。

レビューコメント用のちっさいテキストエリアでは説明しきれないので、なぜそのようなコメントをするのか私なりの持論をまとめてみました。

検証環境

記事内のサンプルコードは下記の環境で検証したものです。環境によりサンプルコードに記載したアウトプットが少し異なる事があります。ご了承ください。

  • Node v15
  • Chrome v91
  • Firefox v89

undefined とは何か

undefined は JavaScript のグローバル上に存在する変数です。in 演算子を使うとグローバルに undefined が存在している事が分かります。

// Node.js(ブラウザでのアウトプットは異なります)

// undefinedはグローバルに存在する
undefined in globalThis
> true

よく使う下記のようなコードは、グローバル上に存在する undefined という変数の値を代入する事を意味しています。

const x = undefined;

// これと同じ
const x = globalThis.undefined;

undefined | MDN

null とは何か

JavaScript には undefined の他に、空を示す概念として null も存在します。

null はあらゆるオブジェクトの派生元です。Object.getPrototypeOf() で派生元を調べると null に行き着くことが分かります。

// Node.js(ブラウザでのアウトプットは異なります)

// 文字列・数値の派生元はオブジェクト
Object.getPrototypeOf("abc")
> {}
Object.getPrototypeOf(123)
> {}

// オブジェクトの派生元はnull
Object.getPrototypeOf({})
> [Object: null prototype] {}

よく使う下記のようなコードは null というリテラルを代入する事を意味しています。

const x = null;

null | MDN

リテラルとは、データ型を一意に特定することができる値の表現です。文字列リテラル、数値リテラルが分かりやすいのではないかと思います。

// 文字列リテラル
const s1 = "abc";
const s2 = '文字列';

// 数値リテラル
const n1 = 123456;
const n2 = 123_4567;

リテラルとグローバル変数がいまいち分からない場合は下記サイトのコラムを参考にしてみてください。

コラム undefinedはリテラルではない | JavaScriptPrimer

「宣言していない」「undefined の代入」は等価ではない

JavaScript では値を代入する前の変数や、何も返さない関数の戻り値を参照すると undefined が得られます。

let x;
x; // undefined

function fn() {}
fn(); // undefined

上記のケースにおいて undefined は JavaScript が「値が何も設定されていない」事を示すためのものです。開発者が初期化のために undefined を代入しても等価にはなりません。

const user1 = {
  name: "カルテット太郎",
};

const user2 = {
  name: "カルテット花子",
  nickname: "hanako",
};
user2.nickname = undefined;
Object.keys(user1); // ["name"]
Object.keys(user2); // ["name", "nickname"]

"nickname" in user1; // false
"nickname" in user2; // true

for (const i in user1) { console.log(i) }; // "name"
for (const i in user2) { console.log(i) }; // "name" "nickname"

「宣言していない」状態に戻すには delete 演算子を利用します。

delete user2.nickname;

Object.keys(user2); // ["name"]
"nickname" in user2; // false
for (const i in user2) { console.log(i) }; // "name"

delete | MDN

このように「宣言前の空の値が undefined 、宣言後の空の値が null 」と区別するのは誤った理解と言えます。

厳密等価と等価

JavaScript には明示的な型宣言はありませんが、内部では「データ型」というものがあります。

JavaScript のデータ型とデータ構造 | MDN

厳密等価(===)はデータ型を変換せず一致を検査します。

"" === 0 // false
"0" === 0 // false
null === undefined // false
undefined === 0 // false
null === 0 // false

厳密等価 | MDN

等価(==)はデータ型の変換後に一致を検査します。

"" == 0 // true
"0" == 0 // true
null == undefined // true
undefined == 0 // false
null == 0 // false

等価 | MDN

ESLint に等価演算子を禁止するためのルールがあるように、特に理由がなければ厳密等価を利用したほうがコードの可読性が上がるでしょう。

function something(n) {
  // n が "000" の時どのような結果になるのか曖昧
  // そもそもこの関数は文字列を許容するのか?
  return n == 0;
}
function something(n) {
  // 0 以外は全て false 判定になる事が明確
  return n === 0;
}

Disallow Null Comparisons (no-eq-null) | ESLint

厳密等価と等価の例から分かるように、冒頭の if 文は undefined のみを想定したガードです。

// x の値が null, 0, "" の時はガードの対象外
if (x === undefined) {
  return;
}

DOM によって意図せず変化する undefined

ここからは x === undefined でキャッチできないバリデーションについての説明です。まず、シンプルな HTML のフォームを例とします。

<form>
  <input type="text">
  <button type="submit">submit</button>
</form>

input に何も入力せずに submit ボタンをクリックした場合、下記のような undefined によるバリデーションは意味をなしません。なぜなら input.value は文字列で、何も入力しない場合に得られる値は空文字列の "" だからです。

document.querySelector('form').addEventListener("submit", (event) => {
  const input = document.querySelector('input');

  // このバリデーションで未入力を検出する事はできない
  if (input.value === undefined) {
    window.alert("入力されていません");
    event.preventDefault();
  }
});

また何らかのフラグによって DOM を出し分けている場合、存在しない要素に対して querySelectornull を返却します。そのため undefined によるバリデーションは同じく意味をなしません。

if (xxx === true) {
  document.querySelector('form').appendChild(
    document.createElement("input")
  );
}

document.querySelector('form').addEventListener("submit", (event) => {
  const input = document.querySelector('input');

  // input は null のため、このバリデーションで submit を中断する事はできない
  if (input === undefined) {
    window.alert("入力されていません");
    event.preventDefault();
  }
});

さらに input に初期値として undefined を与えた場合 "undefined" という文字列が画面に表示されてしまいます。これを嫌って開発者が null を与えているかもしれません。

// inputに "undefined" という文字列が現れる
const initialValue = undefined;
document.querySelector("input").value = initialValue;

// inputに "" という文字列が現れる
const initialValue = null;
document.querySelector("input").value = initialValue;

上記のようなコードを変数に保存して使い回す場合、初期値として与えた undefinednull"" に変化しているかもしれません。

// データの初期化
let data = {
  inputValue: undefined;
};

// 画面にレンダリング
render(data);

// ユーザーが入力した値を取得
data = getUserInputData();

if (data.inputValue === undefined) {
  throw new Error("入力してください");
}

// 保存
persistUser(data);

コードによって意図せず変化する undefined

平均値を算出するコードを例とします。

function calcAverage(...array) {
  return array.reduce((a, b) => a + b, 0) / array.length;
}

calcAverage(30, 35, 45, 26); // 34

数値の列挙には undefined を想定するものとします。この場合 NaN が返却されるため isNaN によるバリデーションを追加しました。

const result = calcAverage(30, 35, 45, 26, undefined); // NaN

if (isNaN(result)) {
  throw new Error("意図しないパラメータが与えられました");
}

ところが今度は数値の列挙に何らかの理由で null が紛れ込みました。 null は数値の 0 とみなされるため、計算結果は NaN にはなりません。計算結果は数値で返却されるため、何も問題が起こっていないかのように計算処理は続行します。

const result = calcAverage(30, 35, 45, 26, null); // 27.2

// このバリデーションではエラーを検出できない
if (isNaN(result)) {
  throw new Error("意図しないパラメータが与えられました");
}

「何らかの理由」というのがイメージしづらいかもしれませんが null は API レスポンスや開発者による初期化処理など、さまざまな箇所で発生します。

// ==============================
// API レスポンスの仕様の取り決め
// ==============================
[User Response] {
  id: number
  name: string
  age: number (optional)
}

- `GET /users` [User Response] のコレクションを返す
- `GET /users/:id` [User Response] を単体のデータとして返す

バックエンドが Node.js で開発されているようなケースを除き、API の開発者はオプショナルの二通りの表現方法「項目そのものがない事」または「項目は存在し値が null である事」によって JavaScript にどんな影響を与えるのか知り得ないでしょう。リリース当初 undefined だったレスポンスの値が、バックエンドのリファクタやライブラリの差し替えによって null に変わるかもしれません。もし null が与えられたらどう振る舞うのか、想定しておいても損はしないはずです。

fetch(`/users/${id}`).then(response => {
  const json = response.json();

  return {
    id: Number(json.id),
    name: json.name,
    age: json.age === undefined ? 0 : json.age,
  };
});

// リリース当初は user.age は undefined だったため、0 で初期化されていた
// いつの間にか user.age は null に変わり、0 で初期化されなくなった

面倒だから全部 null で初期化しちゃえば?

undefined が意図せず null に変化するのなら、最初から全部 null で初期化すれば解決するのかと言うと、そうでもありません。例えば Array.find は該当の要素が見つからない場合に undefined を返却します。

let user = null;

function findUser(id) {
  [
    { id: 1, name: "a" },
    { id: 2, name: "b" },
  ].find(user => user.id === id);
}

user = findUser(1000); // undefined

// このバリデーションは意味をなさない
if (user === null) {
  throw new Error("ユーザーが見つかりません");
}

TypeScript のような型のある言語を使えば、フロントエンドのコード内のミスを防ぐ事はできるでしょう。

interface User {
  id: number;
  name: string;
  optionalValue?: number;
  nullableValue: number | null;
}

const user1: User = {
  id: 1,
  name: "a",

  // 型定義に違反する値は代入できない
  optionalValue: null, // [ERROR] optional に null は代入できない
  nullableValue: undefined, // [ERROR] nullable に undefined は代入できない
};

ただし型アサーション( as によるキャスト)や API レスポンスなど、ランタイム(TypeScript から JavaScript にトランスパイルした後のコード)で代入される値については防ぎようがありません。

// DOM を経由した値など as でキャストする例
const onChangeEventHandler = (target) => {
  const value = target as string;
}

// 実際には HTMLElement が代入されたとしてもコンパイルエラーは発生しない
console.log(value); // HTMLInputElement {...}
// API レスポンスの値をそのまま使用する例
function parse(response): { id: number, value: number } {
  return {
    id: response.id,
    value: response.value
  };
}

// 期待する number でなく string が代入されたとしてもコンパイルエラーは発生しない
console.log(response); // { id: "0001", value: "1,000円" }

つまり undefined または null どちらに統一しようが、代入される値を束縛する事はできません。厳密等価による判定は、局所的なガードになってしまうのです。

function getUser(id) {}
function parse(user) {}
function render(user) {}

const user = parse(getUser(1));

render(user).then(result => {
  // DOM を経由した場合、空文字列かもしれない
  if (result === "") {}

  // Array.find を経由した場合、undefined かもしれない
  if (result === undefined) {}

  // 初期化ロジックを経由した場合、null かもしれない
  if (result === null) {}
});

結局どうして欲しいのか

ここからは、抜け漏れをできるだけ防ぐために気をつけるポイントを紹介します。TypeScript のコードを紹介していますが、それぞれのポイントで言いたい事は JavaScript でも同じです。

宣言する型の範囲はできるだけ狭くする

開発者が自分自身のコードに課す型宣言は、できるだけ範囲を狭くしてください。

// Optional によって null でなく undefined を扱う事を宣言
interface Params {
  url: string;
  queryStrings?: { [key:string]: string }[]; // undefined を使う事を宣言
}
function request(params: Params) {}

request({
  url: "http://example.com",
  queryStrings: null, // [ERROR] 誤った null の代入
);
// NG
// undefined でも null でも何でも代入できてしまう曖昧さ
interface Params {
  url: string;
  queryStrings: any;
}

// NG
// Optional を使い過ぎると何が必要なのか分からなくなる
interface Params {
  id?: string;
  url?: string;
  queryStrings?: object;
}

受け取る値の範囲はできるだけ広くする

ランタイムで値が代入される箇所、または複数のロジックを経由して到達した箇所では、できるだけ広い範囲の値を扱えるようにしてください。

// undefined / null / 空文字列 どれを受け取っても同じ結果になる
// (0 も対象になるため数値の扱いには気をつける事)
function something(value: string | null) {
  if (!value) {
    throw new Error("パラメータが不正です");
  }
}
// undefined / null を等価に扱う例
interface Param {
  foo?: {
    bar: { value: number } | null
  }
}

function something(param: Param): number {
  // Optional chaining(?.) 
  // undefined / null どちらでもオブジェクトのプロパティを辿れる
  let x = param.foo?.bar?.value;

  // Logical nullish assignment(??=) 
  // undefined / null いずれかであれば右辺の値を代入する
  x ??= 1;

  return x + 100;
}
// NG
// undefined / null が考慮されていない
function something(value: string) {
  if (value === "") {
    throw new Error("パラメータが不正です");
  }
}

暗黙の型を避ける

暗黙の型を返却せず、どのような型を返却するかを明確に宣言してください。

interface User {
  id: number;
  address: string | null;
}

// undefined を null に変換するコードが強制される
function pickupAddress(user: User, users: User[]): string | null {
  return (users.find(user => user.id)?.address) || null;
}
// NG
// 戻り値の型を宣言していない
// string | null | undefined と推論される
function pickupAddress(user: User, users: User[]) {
  return users.find(user => user.id)?.address;
}

// 型宣言が面倒になるので後続の処理が any や Optional になりがち
const address = pickupAddress();

終わりに

undefined null をきちんと区別したコードを書くべきとする持論もあるでしょう。

経験に基づく主張ですが、どのような場面で変数の値が undefined null に変化するのか熱心に気を配っていても、想定外の値は混入します。そして、それが原因で想定外のエラーが発生した場合、不利益を被るのはシステムを利用しているユーザーです。デバッグすると想定外に何故か現れる null 値、そしてコードを追いかけると見つける if (x === undefined) によるバリデーション。システムリリース後の不具合報告で、何回もこのパターンに出くわした事があります。

undefinednull がそれぞれ「未登録」や「ユーザーによる値の消去」など意味を持つのであれば話は違いますが、ほとんどのケースにおいて undefinednull は「空」という等価な意味合いではないでしょうか?

x === undefined の比較をしないでください。

これは JavaScript を知り尽くしたような顔をしてミスを指摘しているのではなく、提案としてのレビューコメントです。想定外に紛れ込む null を考慮しておいたほうがメリットが大きいと思うのですが、いかがでしょうか?どうか、ご検討いただければと思います。


このエントリーをはてなブックマークに追加

先週SymfonyWorld 2021 Summer Editionのトークで @TobiasNyholm さんが紹介していたRuntimeコンポーネントを早速使ってみたのでご紹介します!

Runtimeコンポーネントとは

Symfony5.3から導入された新しいコンポーネントです。
実行環境(php-fpm, AWS Lambda, PaaS, swoole, …)ごとのフロントコントローラの違いをクラスとして提供してくれます。
https://symfony.com/doc/current/components/runtime.html

Runtimeコンポーネントで何が嬉しいか?

Runtimeコンポーネント導入前まで、 symfony new して作ったSymfonyプロジェクトは、スタンダードなApache・nginxで動かすことを前提にした public/index.php だけが配布されていました。 それ以外の特殊な実行環境(たとえば「SymfonyをAWS Lambdaで」とか)でSymfonyを動かそうと思ったら、 $_SERVER を自前でセットする等のやり方をググって、自力で public/index.phpbin/console をカスタマイズする必要がありました。

Runtimeコンポーネントは各実行環境ごとのカスタマイズ内容を、再利用可能な形でSymfonyコンポーネントとして提供してくれるため、自力でカスタマイズを適用する必要はなくなりました :relaxed:

既存のSymfonyプロジェクトにRuntimeコンポーネントを導入する方法

前提として、Symfony5.3まで更新してあるプロジェクトが対象です。

symfony/runtime導入

composer require symfony/runtime

vendor/autoload_runtime.php作成, public/index.phpの更新

composer recipes:install symfony/framework-bundle -v --force

config/framework.yamlに自分の設定を書き込んでいる場合は、設定が初期状態まで巻き戻ってしまうため、git diffを見て自分の設定した部分は復元しましょう。

bin/consoleの更新

composer recipes:install symfony/console -v --force



必要な変更をコミット後、 symfony server:start してみると、今まで通りにアプリケーションが動くことを確認できると思います(私はできました)。
vendor/autoload_runtime.php を見てみると、デフォルトのRuntimeとして Symfony\Component\Runtime\SymfonyRuntime が指定されていること、Runtimeを変更したい場合は $_SERVER['APP_RUNTIME'] を変更すれば良いことがわかります。

<?php

// autoload_runtime.php @generated by Symfony Runtime

if (true === (require_once __DIR__.'/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) {
    return;
}

if (PHP_VERSION_ID < 80000 && in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
    ob_start();
    $app = require $_SERVER['SCRIPT_FILENAME'];
    ob_end_clean();
} else {
    $app = require $_SERVER['SCRIPT_FILENAME'];
}

if (!is_object($app)) {
    throw new TypeError(sprintf('Invalid return value: callable object expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME']));
}

$runtime = $_SERVER['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime';
$runtime = new $runtime(($_SERVER['APP_RUNTIME_OPTIONS'] ?? []) + [
  'project_dir' => dirname(__DIR__, 1),
]);

[$app, $args] = $runtime
    ->getResolver($app)
    ->resolve();

$app = $app(...$args);

exit(
    $runtime
        ->getRunner($app)
        ->run()
);

感想

ますますコアなアプリケーション部分の開発に集中できる方向に進化してくれるSymfonyがやっぱり大好きだな〜と思いました。


このエントリーをはてなブックマークに追加

Truncation, Flexbox, and Tables

Truncation

When you have a long string of text that you want to restrict to one line, but not let it grow forever and ever, you have to truncate it. Truncating text can be a nightmare. The standard way of truncating text is with a CSS component like this:

.truncate {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

Then you just have to apply it to an element with text.

It Just Works

Playground Link →

<div class="max-w-md flex gap-16 w-full mx-auto justify-center">
  <p class="truncate">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sodales
    elit quis vulputate pretium. In hac habitasse platea dictumst. Praesent
    scelerisque dolor sit amet volutpat aliquam. Phasellus ultrices sollicitudin
    metus. Cras sit amet sem nibh. Pellentesque lacinia enim eget lacinia
    dignissim. Donec iaculis fringilla suscipit. Phasellus non lobortis quam. Ut
    rutrum ex eu suscipit dignissim. Fusce elementum rutrum risus sit amet
    posuere. Sed ac sollicitudin arcu. Mauris interdum lorem sed mi sodales
    placerat.
  </p>
</div>

This just works. All you need is the .truncate class and for the parent of the text block to have a width or max-width. The browser takes care of everything. This starts to break a little when you’re using flexbox though.

Flexbox Fail

Playground Link →

<div class="max-w-md flex gap-16 w-full mx-auto justify-center">
  <div>
    <p class="truncate">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
      sodales elit quis vulputate pretium. In hac habitasse platea dictumst.
      Praesent scelerisque dolor sit amet volutpat aliquam. Phasellus ultrices
      sollicitudin metus. Cras sit amet sem nibh. Pellentesque lacinia enim eget
      lacinia dignissim. Donec iaculis fringilla suscipit. Phasellus non
      lobortis quam. Ut rutrum ex eu suscipit dignissim. Fusce elementum rutrum
      risus sit amet posuere. Sed ac sollicitudin arcu. Mauris interdum lorem
      sed mi sodales placerat.
    </p>
  </div>
</div>

Min-Width Trick

If the text you’re trying to truncate is inside a flex child, the truncate trick won’t work. The way around this is to apply min-width: 0px to the flex child.

Playground Link →

<div class="max-w-md flex gap-16 w-full mx-auto justify-center">
  <div class="min-w-0">
    <p class="truncate">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
      sodales elit quis vulputate pretium. In hac habitasse platea dictumst.
      Praesent scelerisque dolor sit amet volutpat aliquam. Phasellus ultrices
      sollicitudin metus. Cras sit amet sem nibh. Pellentesque lacinia enim eget
      lacinia dignissim. Donec iaculis fringilla suscipit. Phasellus non
      lobortis quam. Ut rutrum ex eu suscipit dignissim. Fusce elementum rutrum
      risus sit amet posuere. Sed ac sollicitudin arcu. Mauris interdum lorem
      sed mi sodales placerat.
    </p>
  </div>
</div>

Tables

The above methods should let you truncate text in every situation, except tables. Because…tables.

The Problem with Tables

Playground Link →

<table>
  <thead>
    <tr>
      <th class="p-2 text-left">Flexible Width</th>
      <th class="p-2 text-left">Short Width</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="p-2 truncate">
        Bacon ipsum dolor amet meatball ground round chicken, hamburger cupim
        sirloin doner burgdoggen porchetta capicola kevin ham.
      </td>
      <td class="p-2">
        <div class="flex items-center gap-4">
          <button class="bg-blue-600 text-white rounded-md shadow py-1 px-3">
            Button
          </button>
          <button class="bg-blue-600 text-white rounded-md shadow py-1 px-3">
            Button
          </button>
        </div>
      </td>
    </tr>
  </tbody>
</table>

The problem you’ll run into with tables is that when you have a td, it has no real width. It just takes up the space it’s given. If you apply our .truncate class, the text in the cell will explode outside of the td. You can give it a width or give it a max-width, and it works, but you lose the flexible sizing the browser gives you by default.

Playground Link →

<table>
  <thead>
    <tr>
      <th class="p-2 text-left">Flexible Width</th>
      <th class="p-2 text-left">Short Width</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="p-2 max-w-sm truncate">
        Bacon ipsum dolor amet meatball ground round chicken, hamburger cupim
        sirloin doner burgdoggen porchetta capicola kevin ham.
      </td>
      <td class="p-2">
        <div class="flex items-center gap-4">
          <button class="bg-blue-600 text-white rounded-md shadow py-1 px-3">
            Button
          </button>
          <button class="bg-blue-600 text-white rounded-md shadow py-1 px-3">
            Button
          </button>
        </div>
      </td>
    </tr>
  </tbody>
</table>

The Table Cell Fix

The fix for table cells without a width/max-width is not that difficult to implement, but there’s a trick to making it flexible. You need to make the td a flex container, wrap your content in a div that has flex: 1 1 0% and width: 0px set, and then add your content inside with our .truncate class. The flex settings might make sense, but the width is weird. You need to tell the width to be 0px so that it will be as small as possible, but can flex as large as there is space available.

Playground Link →

<table class="min-w-full w-full">
  <thead>
    <tr>
      <th class="p-2 text-left">Flexible Width</th>
      <th class="p-2 w-0 text-left">Short Width</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="mt-1.5 p-2 flex">
        <div class="flex-1 w-0 truncate">
          Bacon ipsum dolor amet meatball ground round chicken, hamburger cupim
          sirloin doner burgdoggen porchetta capicola kevin ham.
        </div>
      </td>
      <td class="p-2">
        <div class="flex items-center gap-4">
          <button class="bg-blue-600 text-white rounded-md shadow py-1 px-3">
            Button
          </button>
          <button class="bg-blue-600 text-white rounded-md shadow py-1 px-3">
            Button
          </button>
        </div>
      </td>
    </tr>
  </tbody>
</table>

There’s a slight problem with this method. Table cells do not want to be flex containers. They lose their default vertical alignment. They do not know how tall the row they are in is, so you cannot make them fill the tr’s height. You have to add margin-top by sight to make the td’s content vertically centered. It’s not great, but it is the best you can do.

And One More Thing…

If for some reason you have a fieldset inside your new truncate/flex combo, it will not work.

Playground Link →

<td class="mt-1.5 p-2 flex">
  <div class="flex-1 w-0">
    <fieldset class="">
      <span class="block truncate">
        Bacon ipsum dolor amet meatball ground round chicken, hamburger cupim
        sirloin doner burgdoggen porchetta capicola kevin ham.
      </span>
    </fieldset>
  </div>
</td>

The problem is fieldsets do not behave like other normal elements. You can make it work though. The trick is making the fieldset be min-width: 0px. This will trick the fieldset into shrinking like other elements can because a fieldset has a standard min-width of whatever its content is.

Playground Link →

<td class="mt-1.5 p-2 flex">
  <div class="flex-1 w-0">
    <fieldset class="min-w-0">
      <span class="block truncate">
        Bacon ipsum dolor amet meatball ground round chicken, hamburger cupim
        sirloin doner burgdoggen porchetta capicola kevin ham.
      </span>
    </fieldset>
  </div>
</td>

Truncation is a necessary tool for many UIs. Hopefully, if you find yourself needing it in a table, you’ll be able to handle it without any problem in the future!