簡體   English   中英

通過類型強制執行互斥的 ARIA label 屬性並修改 Props 使其不兼容

[英]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 個不同的問題,盡管錯誤消息是相同的:

  1. spread object prop 和 inline prop 的混搭
  2. 使用Omit修改后的不兼容類型

1.混搭

當我混合搭配擴展運算符和 JSX 屬性時,出現編譯錯誤

問題是,通過在<Dropdown>組件上添加顯式label道具,道具的 rest 應該“永遠”包含ariaLabelariaLabelledBy道具(由LabelWithAria類型指定)。

但是這些道具的“其余部分”是從 object 傳播的,它是一個完整的DropdownProps類型,因此它很可能包含這些屬性之一。


2.不兼容的類型一旦用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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM