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只能是nullunits只是空陣列err只能是null
可是你後面實際上會這樣寫:
1
2
3
4
5
setState({
loadedYear: uyear,
units: normalized,
err: null,
});
這時 IDE 就會說:
- 你一開始不是說
loadedYear是null嗎? - 為什麼現在塞
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 ReactNodestate被推成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 型別系統背起來。
你目前只要懂這三件事:
@typedef是幫資料結構取名字@param/@returns是在告訴 IDE 函式收什麼、回什麼/** @type {...} */是在告訴 IDE 某個變數不要亂猜
能看懂這三個,你就已經夠用。
10. 那我是不是乾脆去學 TypeScript
如果這專案還會繼續長,答案是:
- 建議
因為你現在一直在補的這些 JSDoc, 本質上就是「還沒全面轉 TS 前的過渡方案」。
你學會 TS 之後,再回頭看這些東西,會容易很多:
@typedef類似type/interface@param類似函式參數型別@type類似變數型別註記
所以:
- 這篇是讓你先在現在的 JS 專案活得下去
- 不代表 JSDoc 是長期終點