Post

IDEA Warning JS Type

IDEA Warning JS Type

1. 為什麼純 JavaScript 也會一直看到型別 warning

因為現在前端工具鏈早就不把 JavaScript 當成「隨便寫都行」。

即使你沒有把檔案改成 .ts/.tsx,IDE 仍然會嘗試從這些地方推型別:

  • useState(...) 的初始值
  • 函式參數
  • 函式回傳值
  • map(...) 的來源陣列
  • JSDoc,例如 @param@returns@typedef

所以現在的狀態其實是:

  • 語言本身還是 JS
  • 工具已經開始要求型別資訊
  • 如果你不講清楚,IDE 就會自己猜
  • 猜錯,就開始冒出 warning

這就是現在這種「半套型別化」的來源。


2. 這不是完整 TypeScript,而是過渡方案

你現在看到的這些寫法:

1
2
3
4
5
6
/**
 * @typedef {object} LeaderUnit
 * @property {string} unitSn
 * @property {string} unitName
 * @property {string} degree
 */

或:

1
2
3
4
5
6
/** @type {SignableUnitsState} */
const initialState = {
  requestKey: "",
  units: [],
  err: null,
};

本質上是在做一件事:

  • 用 JSDoc 告訴 IDE:「不要亂猜,我這裡的型別是什麼」

所以它不是正規 TS,但它是在補 JS 專案的型別洞。


3. 最常看到的三種補法

3.1 @typedef

用途:

  • 幫資料結構取名字

例如:

1
2
3
4
5
6
7
8
/**
 * 主管身分資料。
 *
 * @typedef {object} LeaderIdentityUnit
 * @property {string} unitSn 單位序號
 * @property {string} unitName 單位名稱
 * @property {string} degree 主管層級
 */

這樣後面就可以直接寫:

1
2
3
/**
 * @param {{ open: boolean, units: LeaderIdentityUnit[], onSelect: (unit: LeaderIdentityUnit) => void }} props
 */

不用每次都把整坨物件型別重寫一次。

3.2 @param / @returns

用途:

  • 告訴 IDE 這個函式收什麼、回什麼

例如:

1
2
3
4
/**
 * @param {number|string} uyear 學年度
 * @returns {{ units: string[], loading: boolean, err: string | null }}
 */

3.3 /** @type {...} */

用途:

  • 告訴 IDE 某個變數的型別,不要只靠初始值猜

這個在 useState 很常用。


4. 實際例子一:useLeaderUnits.js

你遇到的 warning 長這樣:

1
2
3
4
5
6
Argument type {
  loadedYear: number | string,
  units: ...[],
  err: null
} is not assignable ...
Type number | string is not assignable to type null

為什麼會這樣

原本寫法是:

1
2
3
4
5
const [state, setState] = useState({
  loadedYear: null,
  units: [],
  err: null,
});

IDE 很容易推成:

  • loadedYear 只能是 null
  • units 只是空陣列
  • err 只能是 null

可是你後面實際上會這樣寫:

1
2
3
4
5
setState({
  loadedYear: uyear,
  units: normalized,
  err: null,
});

這時 IDE 就會說:

  • 你一開始不是說 loadedYearnull 嗎?
  • 為什麼現在塞 number | string

修法

先定義 state 型別:

1
2
3
4
5
6
/**
 * @typedef {object} LeaderUnitsState
 * @property {number|string|null} loadedYear
 * @property {LeaderUnit[]} units
 * @property {string|null} err
 */

再明確標註初始值:

1
2
3
4
5
6
7
8
/** @type {LeaderUnitsState} */
const initialState = {
  loadedYear: null,
  units: [],
  err: null,
};

const [state, setState] = useState(initialState);

重點

不是程式不能跑。

是 IDE 原本把初始值猜得太窄了,所以你要把真正可能的型別講清楚。


5. 實際例子二:useSignableUnits.js

你遇到的 warning 是:

1
2
3
4
5
Returned expression type {
  units: [] | any[],
  loading: boolean,
  err: null | null
} is not assignable ...

為什麼會這樣

原因和 useLeaderUnits.js 一樣:

  • useState 初始值太窄
  • @returns 又太鬆

例如:

1
2
3
4
5
const [state, setState] = useState({
  requestKey: "",
  units: [],
  err: null,
});

修法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * @typedef {object} SignableUnitsState
 * @property {string} requestKey
 * @property {string[]} units
 * @property {string|null} err
 */

/** @type {SignableUnitsState} */
const initialState = {
  requestKey: "",
  units: [],
  err: null,
};

再把回傳型別也講明:

1
2
3
/**
 * @returns {{ units: string[], loading: boolean, err: string | null }}
 */

6. 實際例子三:ApprovalFlowChart.jsx

你遇到的 warning 是:

1
Type unknown[] is not assignable to type React.ReactNode

而且卡在這種地方:

1
2
3
{flowStages.map((stage) => (
  ...
))}

為什麼會這樣

因為 IDEA 沒有正確理解:

  • flowStages 是什麼陣列
  • stage 是什麼物件

它一旦把 flowStages 推成 unknown[], 整個 map(...) 的結果也會變成 unknown[]

但 JSX 需要的是:

  • ReactNode
  • JSX.Element[]

所以它就開始報錯。

修法

先把資料項目型別定義好:

1
2
3
4
5
6
7
8
/**
 * @typedef {object} ApprovalFlowStage
 * @property {string} key
 * @property {string} title
 * @property {string} status
 * @property {string} summary
 * @property {string | null | undefined} detail
 */

再把節點陣列明確落型別:

1
2
3
4
5
6
/** @type {import("react").JSX.Element[]} */
const flowStatusNodes = flowStages.map((stage) => (
  <Box key={stage.key}>
    {stage.summary}
  </Box>
));

然後 JSX 裡直接用:

1
{flowStatusNodes}

重點

這不是 React 壞掉。

是 IDE 沒看懂 map 產生的是什麼東西,所以你明白告訴它:

  • 這是一串 JSX.Element[]

它就閉嘴了。


7. 實際例子四:LeaderIdentityDialog.jsx

同樣是這種 warning:

1
Type unknown[] is not assignable to type React.ReactNode

原本是:

1
2
3
4
5
6
7
<Stack spacing={1.5}>
  {units.map((unit) => (
    <Button key={`${unit.unitSn}-${unit.degree}`}>
      ...
    </Button>
  ))}
</Stack>

修法

先做:

1
2
3
4
5
6
/** @type {import("react").JSX.Element[]} */
const unitNodes = units.map((unit) => (
  <Button key={`${unit.unitSn}-${unit.degree}`}>
    ...
  </Button>
));

再 render:

1
2
3
<Stack spacing={1.5}>
  {unitNodes}
</Stack>

8. 哪些 warning 可以先忽略,哪些不行

可以先視為工具型 warning

例如:

  • unknown[] is not assignable to ReactNode
  • state 被推成 null 或空陣列
  • @returns {JSX.Element} / React.JSX.Element / import("react").JSX.Element 這類 IDE 型別差異

這些通常是:

  • IDE 不夠懂
  • JSDoc 沒講清楚

不該隨便忽略的

例如:

  • 真的可能 NullPointerException / null 存取錯
  • API 呼叫參數型別明顯不合
  • Promise 沒處理
  • 回傳值與實際邏輯明顯矛盾

例如你之前看到那支沒引用的 dataGrid.jsx

  • RecordAPI.getRecords({...}) 的呼叫方式就和專案實作不符

那種就不是「單純 IDE 在吵」,而是檔案本身品質就有問題。


9. 你現在需要學到什麼程度

不用把 JSDoc 型別系統背起來。

你目前只要懂這三件事:

  1. @typedef 是幫資料結構取名字

  2. @param / @returns 是在告訴 IDE 函式收什麼、回什麼

  3. /** @type {...} */ 是在告訴 IDE 某個變數不要亂猜

能看懂這三個,你就已經夠用。


10. 那我是不是乾脆去學 TypeScript

如果這專案還會繼續長,答案是:

  • 建議

因為你現在一直在補的這些 JSDoc, 本質上就是「還沒全面轉 TS 前的過渡方案」。

你學會 TS 之後,再回頭看這些東西,會容易很多:

  • @typedef 類似 type / interface
  • @param 類似函式參數型別
  • @type 類似變數型別註記

所以:

  • 這篇是讓你先在現在的 JS 專案活得下去
  • 不代表 JSDoc 是長期終點
This post is licensed under CC BY 4.0 by the author.