为啥允许使用 kebab-case 非标准属性,而不允许使用其他属性?以及如何在 TypeScript 中定义这样的类型?

Posted

技术标签:

【中文标题】为啥允许使用 kebab-case 非标准属性,而不允许使用其他属性?以及如何在 TypeScript 中定义这样的类型?【英文标题】:Why kebab-case non-standard attributes are allowed while others aren't? And how to define types like this in TypeScript?为什么允许使用 kebab-case 非标准属性,而不允许使用其他属性?以及如何在 TypeScript 中定义这样的类型? 【发布时间】:2021-08-12 20:35:53 【问题描述】:

使用foo 作为属性会引发错误:

// App.tsx
//                     ???? throws
const App = () => <div foo></div>

export default App
Type ' foo: true; ' is not assignable to type 'DetailedhtmlProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
  Property 'foo' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.ts(2322)

但是使用foo-foo 没问题,这是为什么呢?

// App.tsx
//                     ???? no error is thrown
const App = () => <div foo-foo></div>

export default App

最重要的是,如何在 TypeScript 中定义这样的类型?即只允许标准或 kebab-case 属性。

【问题讨论】:

有趣的是,我已经看到将 kebab case 道具添加到元素以进行测试是一种常见的做法。通常像&lt; element data-test="Div" /&gt; 这样你就可以找到这个带有 Enzyme 或 React 测试库的数据测试标签。我从来没想过要问为什么kebab case不会导致元素出错。 【参考方案1】:

答案在JSX section of the Typescript Handbook:

如果属性名称不是有效的JS identifier(如data-* 属性),则如果在元素属性类型中找不到它,则不会将其视为错误。

关于 JSX 的整个部分是一本非常有启发性的读物。

恐怕“如何在 TypeScript 中定义这样的类型”问题的答案是......我们没有。 JSX 支持内置在编译器中。不过,我们可以自定义很多有趣的东西。

【讨论】:

【参考方案2】:

法比奥的答案中的 TS 手册说明解释了所有这些,只是想稍微扩展一下。简而言之,kebab-case 属性不被 TS 认为有效但不会抛出错误;但以data-aria- 为前缀的属性被认为是有效的。

React (since 16) 接受自定义属性,即 &lt;div foo /&gt;&lt;div whateverYouLike=2&gt; 应该可以工作。

我对 React 感到困惑的是,data-*aria-* 应该按原样编写,而不是像其他所有内容一样将它们转换为 camelCase。尤其是当这些属性在 vanilla DOM 中转换为 camelCase 时:

<div data-my-age="100" aria-label="A Test" />
const $div = document.querySelector('#test')

$div.dataset.myName = "D"
console.log( dataset: $div.dataset ) //  myAge: "100", myName: "D" 
console.log($div.ariaLabel) // "A Test"

没有给出任何理由,所以我们只能推测。也许与 a11y 工具、解析便利等有关。

&lt;div foo /&gt; 在 TS 中抛出的原因是因为 TS 提供了一组严格的有效属性名称。但是,正如另一个答案所指出的,TS 不会在 random-foo 上抛出错误,因为它被认为是无效的 JS 标识符。我的猜测是因为 DOM 元素允许任意属性,所以这是一种折衷方案,允许在 TS 中的大多数情况下正确键入,但提供某种逃生舱口。很想知道这些决定背后的原因。

如何在 TypeScript 中定义这样的类型?即只允许标准或 kebab-case 属性。

正如 Fabio 已经指出的,JSX 支持内置在编译器中。然而,除了能够识别构成有效属性名称的能力之外,我认为它没有什么神奇之处:有一个有效 DOM 属性的综合列表。如果您混合使用 kebab 和 camel case,TS 不会抛出错误,即 &lt;div data-myName&gt; 工作,&lt;div myName/&gt; 没有,等等,所以它也不会因大小写而有所不同。

如果你事先知道所有有效的道具,你可以模仿同样的事情。

// allow only these prop names, which happened to be all camelCased

interface MyThing 
  name: string
  myName: string
  anotherProp: string

在 kebab-case 的情况下,模板文字类型可能会有所帮助:

type ValidPrefix = "data" | "aria";
type ValidSuffix = "banana" | "apple" | "pear";

type ComputedProps = 
    [key in `$ValidPrefix-$ValidSuffix`]?: string;
;

const x: ComputedProps = 
    "data-apple": 'hi'
;

除此之外,TS 中目前没有机制可以区分 camelCase 和 kebab-case 字符串。


如果您正在寻找一种方法来增强 JSX 以允许自定义道具和自定义元素,这是一种方法:

Augmenting JSX attribute to allow custom props & custom elements

【讨论】:

以上是关于为啥允许使用 kebab-case 非标准属性,而不允许使用其他属性?以及如何在 TypeScript 中定义这样的类型?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C# 6.0 在使用 Null 传播运算符时不允许设置非 null 可空结构的属性?

[Effective JavaScript 笔记]第29条:避免使用非标准的栈检查属性

如何通过非标准属性访问输入元素?

为啥使用非标准 C(gcc 特定功能)对 linux 内核进行编码? [关闭]

C语言为啥可以重写标准库函数?

为啥 Python 3.8.0 允许在不使用“非本地”变量的情况下从封闭函数范围更改可变类型?