這篇文章的撰寫緣由是我在React中某一元件要傳入兩種不同的物件陣列:
interface UserInfo {
generalInfo: GeneralInfoTemplate;
educationExps: Array<EducationExpInfoTemplate>;
practicalExps: Array<PracticalExpInfoTemplate>;
}
type UserInfoKeys = Exclude<keyof UserInfo, "generalInfo">;
interface Props {
block: UserInfoKeys;
blockKey: string;
setInputs: React.Dispatch<React.SetStateAction<UserInfo[UserInfoKeys]>>;
setAppUserData: React.Dispatch<React.SetStateAction<UserInfo>>;
}
簡單來說,我想把const [state, setState] = React.useState()
這類的setState
用props傳給子元件,然後這個setState
是設置UserInfo物件中的educationExps「或」praticalExps用的,結果當我使用setInputs
把這兩種之一的值傳進時報錯了。
這個錯誤,也是簡單來說,他說我educationExps沒有praticalExps的屬性,praticalExps沒有educationExps屬性…(怎麼聯集變交互比對了?)
我把這個狀況另外寫個測試重現出來如下:
type Foo = { one: number; two: number };
type Bar = { three: number; four: number };
type Foobar = Foo | Bar;
interface Props {
value: Foobar;
setValue: React.Dispatch<React.SetStateAction<Foobar>>;
}
/** This will be used in App component below */
function Beep(props: Props) {
const boop = Object.assign({}, props.value);
return (
<>
<div>{"one" in boop ? boop.two : boop.four}</div>
</>
);
}
export default function App() {
const [foo, setFoo] = React.useState({ one: 1, two: 2 });
const [bar, setBar] = React.useState({ three: 3, four: 4 });
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
/** setValue will have an error */
<Beep value={foo} setValue={setFoo} />
<Beep value={bar} setValue={setBar} />
</div>
);
}
- 在App的return中,Beep這個元件的屬性
setValue
會報錯。
原因
參考Stackoverflow上的這則回答;先是TypeScript(以下簡稱TS)官網在(舊頁面的)Type inference in conditional types裡提到:
“Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:”
然後回答者給出的整理:
It means, if you want to call such function, arguments of all functions in a union will merge (intersect), that why
setValue
expectsReact.SetStateAction<Animal> & React.SetStateAction<Cat>
.
當在函式的引數中使用聯集(Union)會造成TS判斷為交集。
知道這點後,來試著修正該回覆中舉例會報錯的例子:
除錯
-
一種做法是用Generic type然後用Ternary operator判斷該值是否繼承自
unknown
,是的話要給攤開後foo
和bar
的形式;如果給的是foo | bar
還是會報錯:
declare const foo: (val: { name: string }) => void;
declare const bar: (val: { age: number }) => void;
type ToEither<T> = T extends unknown ? (val: { name: string } | { age: number }) => void : never;
declare const union: ToEither<typeof foo | typeof bar>;
union(foo)
union(bar)
-
或許還不夠簡潔,於是我們可以把
ToEither
的結構改為foo
和bar
的函式形式,然後把foo
、bar
提取出來改成參數:
type ToEither<T> = (val: T) => void;
declare const foo: {name: string}
declare const bar: {phone: number}
declare const union: ToEither<typeof foo | typeof bar>;
當然實務上不見得抽得出來,這時第一種作法就派得上用場了。
至於一開始提到在React的錯誤,用剛才學到的辦法就可以除錯:
interface Props<T> {
value: Foobar;
setValue: React.Dispatch<React.SetStateAction<T>>;
}
function Beep<T extends Foobar>(props: Props<T>) {
const boop = Object.assign({}, props.value);
return (
<>
<div>{"one" in boop ? boop.two : boop.four}</div>
</>
);
}
網友提供的方法
針對一開始的 React 問題,這兩個方法比較單純,但在 inline type 的提示上就沒有上面的方法來的詳細 ,把滑鼠移到 value
和 setValue
上可以看到在這三種方法上的差別:
<Beep value={foo} setValue={setFoo} />
<Beep value={bar} setValue={setBar} />
使用文章中提到的 extends
方法的會是:
(JSX attribute) Props<{ one: number; two: number; }>.setValue: React.Dispatch<React.SetStateAction<{
one: number;
two: number;
}>>
網友提供的一種方法是簡單給 useState
type parameter:
const [foo, setFoo] = React.useState<Foobar>({ one: 1, two: 2 });
const [bar, setBar] = React.useState<Foobar>({ three: 3, four: 4 });
Inline type 會是:
(JSX attribute) Props.setValue: React.Dispatch<React.SetStateAction<Foobar>>
另一種方法是把 type Foobar
的 setValue
property 攤開來:codesandbox link
interface Props {
value: Foobar;
setValue: ((value: Foo) => void) | ((value: Bar) => void);
}
setValue
的 inline type 會是:
(JSX attribute) Props.setValue: ((value: Foo) => void) | ((value: Bar) => void)
文章就到這邊,謝謝閱讀😄