[英]Enforce mutually exclusive ARIA label attributes via types and modify the Props makes it incompatible
當擔心組件中的可訪問性時,應該只存在一個 label:應該有 label、aria-label 或 aria-labelled-by。 (例外情況應該很少見;如果您迫切需要它,則對“任何”進行一些轉換。)我正在嘗試使用螺栓固定類型來強制執行此操作,以使其更容易陷入成功的陷阱:
export type LabelWithAria<LabelType = string> =
| { label?: LabelType; ariaLabel?: never; ariaLabelledBy?: never }
| { label?: never; ariaLabel?: string; ariaLabelledBy?: never }
| { label?: never; ariaLabel?: never; ariaLabelledBy?: string };
然后我將它添加到組件的道具中,如下所示:
export type DropdownProps<TValue = OptionValues> = LabelWithAria & {
value?: DropdownOption<TValue> | DropdownOptions<TValue> | null;
rawValue?: TValue;
...a dozen or so other properties...
}
當我用 JSX 中的所有屬性編寫我的 React 組件時,這很有效,盡管錯誤消息有點冗長(這個最小的重現沒問題,但通常有十幾個道具,這使得這么多更冗長,經常檢查多個重載):
<Dropdown label="foo" ariaLabel="foo, only moreso" />
Type '{ label: string; ariaLabel: string; }' is not assignable to type 'IntrinsicAttributes & PropsWithChildren<DropdownProps<any>>'.
Type '{ label: string; ariaLabel: string; }' is not assignable to type '{ label?: ReactChild | undefined; ariaLabel?: undefined; ariaLabelledBy?: undefined; }'.
Types of property 'ariaLabel' are incompatible.
Type 'string' is not assignable to type 'undefined'.ts(2322)
當一個更復雜的組件將其道具作為道具 object(用於包裝器組件)的一部分傳遞時,問題就來了。 下面的這種用法應該是合法的(“值”無論如何都是可選的)。
export interface InputOrValueProps {
value: string | number | null | undefined | string[] | number[];
selectProps?: Omit<DropdownProps, 'value'>;
...and other properties...
}
public render() {
return <Dropdown {...(this.props.selectProps)} rawValue={this.props.value ?? ''} />;
}
這給出了關於 ariaLabel 的錯誤:
Type '{ rawValue: string | number | string[] | number[]; validationErrors?: ValidationError[] | undefined; label?: ReactChild | undefined; ariaLabel?: string | undefined; ... 32 more ...; }' is not assignable to type 'IntrinsicAttributes & PropsWithChildren<DropdownProps<any>>'.
Type '{ rawValue: string | number | string[] | number[]; validationErrors?: ValidationError[] | undefined; label?: ReactChild | undefined; ariaLabel?: string | undefined; ... 32 more ...; }' is not assignable to type '{ label?: ReactChild | undefined; ariaLabel?: undefined; ariaLabelledBy?: undefined; }'.
Types of property 'ariaLabel' are incompatible.
Type 'string | undefined' is not assignable to type 'undefined'.
Type 'string' is not assignable to type 'undefined'.ts(2322)
此外,當我混合搭配擴展運算符和 JSX 屬性時,我會遇到編譯錯誤:
const props: DropdownProps = {
options: [...],
... and the rest of the props...
};
...and a bunch more logic...
<Dropdown label="I am sad" {...props} />
抱怨是同樣的“ariaLabel is wrong”錯誤:
Type '{ label: string; shouldDisplayLabelInline: true; styleOverrides: { root: { gridTemplateColumns: string; }; }; validationErrors?: ValidationError[] | undefined; ariaLabel?: string | undefined; ... 32 more ...;} | { ...; } | { ...; }' is not assignable to type 'IntrinsicAttributes & PropsWithChildren<DropdownProps<any>>'.
Type '{ label: string; shouldDisplayLabelInline: true; styleOverrides: { root: { gridTemplateColumns: string; }; }; validationErrors?: ValidationError[] | undefined; ariaLabel?: string | undefined; ... 32 more ...;}' is not assignable to type 'IntrinsicAttributes & PropsWithChildren<DropdownMCProps<any>>'.
Type '{ label: string; shouldDisplayLabelInline: true; styleOverrides: { root: { gridTemplateColumns: string; }; }; validationErrors?: ValidationError[] | undefined; ariaLabel?: string | undefined; ... 32 more ...;}' is not assignable to type '{ label?: ReactChild | undefined; ariaLabel?: undefined; ariaLabelledBy?: undefined; }'.
Types of property 'ariaLabel' are incompatible.
Type 'string | undefined' is not assignable to type 'undefined'.
Type 'string' is not assignable to type 'undefined'.ts(2322)
如果道具 object 的類型為Partial<DropdownProps>
,這可以稍微解決一下,但同樣,如果您第一次沒有正確處理,它對程序員不友好,並會出現一條模糊的錯誤消息。
有些問題是當我稍微弄亂類型時,所以包裝器組件采用Omit<DropdownProps, "only" | "optional" | "props" | "omitted">
Omit<DropdownProps, "only" | "optional" | "props" | "omitted">
Omit<DropdownProps, "only" | "optional" | "props" | "omitted">
。
是的,如果我做一堆轉換一切都很好,但我正在制作這些組件供其他人使用。 並且由於這些錯誤消息在指出解決方案方面非常糟糕(添加毫無意義的轉換。),盡管讓 ARIA 標簽“正確”是件好事。 我不想以增加使用這些組件的難度為代價。
<Dropdown {...(this.props.selectProps as DropdownProps)} rawValue={this.props.value ?? ''} />
我想可能可以使用高度冗余的定義來定義 DropdownProps,但這是一個維護噩夢,這意味着我必須為我們使用的 ~20 個不同組件執行此操作,這些組件具有 label 和 ariaLabel(我不是確定它會解決上述傳遞/混合道具的問題):
export type DropdownProps = {
label?: string;
...all the usual props...
} | {
ariaLabel?: string;
...same props again...
} | {
ariaLabelledBy?: string;
...yet a third copy of the same props...
}
實際上,目標只是為 ARIA 屬性提供一種簡單的混合類型,我可以將其附加到組件上,以便於正確使用 ARIA 標簽(在特定組件上不超過一個 ARIA 類型標簽),同時不會使組件更難使用或修改。
您有 2 個不同的問題,盡管錯誤消息是相同的:
Omit
修改后的不兼容類型當我混合搭配擴展運算符和 JSX 屬性時,出現編譯錯誤
問題是,通過在<Dropdown>
組件上添加顯式label
道具,道具的 rest 應該“永遠”包含ariaLabel
或ariaLabelledBy
道具(由LabelWithAria
類型指定)。
但是這些道具的“其余部分”是從 object 傳播的,它是一個完整的DropdownProps
類型,因此它很可能包含這些屬性之一。
Omit
修改有些問題是當我稍微弄亂類型時,所以包裝器組件采用
Omit<DropdownProps, "only" | "optional" | "props" | "omitted">
Omit<DropdownProps, "only" | "optional" | "props" | "omitted">
正如您發現的那樣,一旦您擺弄了您的類型,新類型就會以某種方式與原始類型不兼容,即使它可能仍然有效(只是省略了可選屬性等)。
看起來 TS 不能很好地處理用於互斥屬性的聯合類型的返工,例如您的LabelWithAria
:
// Define mutually exclusive properties
type Xor = {
a: number
b?: never
} | {
a?: never
b: number
}
// @ts-expect-error
const a: Xor = { // Error: Types of property 'a' are incompatible. Type 'number' is not assignable to type 'undefined'.
a: 1,
b: 2
}
// Let's fiddle a bit with the type
type Modified = Omit<Xor, "whatever"> // It should theoretically be the same thing...
// Should have failed!
const m: Modified = { // For some reason, TS no longer enforces the mutual exclusion
a: 1,
b: 2
}
declare const m2: Modified;
// Should have been okay!
const b: Xor = m2; // Error: Type 'Modified' is not assignable to type 'Xor'.
一個可能的解決方案(針對這兩個問題)可能是拆分DropdownProps
以保持 Xor 部分獨立,並且僅在“正常”道具上工作。
不幸的是,這意味着消費組件必須處理這些單獨的定義,然后重新合並它們。
也可以提供輔助類型:
// Helper type
type WithAriaLabel<Props> = LabelWithAria & Props;
type DropdownPropsNormal = {
options?: {label: string; value: string}[];
}
type DropdownProps = WithAriaLabel<DropdownPropsNormal>
type MixedProps = {
wrapped: DropdownPropsNormal; // label is always provided externally, so wrapped cannot contain any ariaLabel/ariaLabelledBy => DropdownPropsNormal only
}
const MixedRepro: React.FunctionComponent<MixedProps> = (props: MixedProps) => {
return <Dropdown {...props.wrapped} label="foo"/> // Okay
}
const PartialPropsRepro: React.FunctionComponent = (props) => {
const spread: WithAriaLabel<Omit<DropdownPropsNormal, "options">> = {
label: "whatTheFoo"
}
return <Dropdown {...spread}/> // Okay
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.