Using foo
as an attribute throws an error:
// 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)
But using foo-foo
is fine, why's that?
// App.tsx
// 👇 no error is thrown
const App = () => <div foo-foo></div>
export default App
And most importantly, how to define types like this in TypeScript? i.e. Only allowing standard or kebab-case attributes.
Kebab case is the way to write compound words separated by hyphens (-) instead of using space. Generally, everything is written in lowercase. Example: “what-is-kebab-case”
Kebab case vs. snake case. Kebab case is a similar naming convention to snake case -- or snake_case. Both conventions help a developer read code because the white space -- either a dash in kebab case or an underscore in snake case -- between words reads like a normal sentence. Video Player is loading. This is a modal window.
Kebab case is the way to write compound words separated by hyphens (-) instead of using space. Generally, everything is written in lowercase. Example: “what-is-kebab-case” This writing style is widely used in SEO (Search Engine Optimization – technique used by software developers to optimize search engines) to create a “slug” in URLs.
If a developer uses all uppercase letters in a variable with the kebab case convention, it's known as a scream kebab. The term owes its name to the thought that when people on social media type sentences with upper case letters, they're screaming. For example: The biggest problem with kebab case lies mainly on the use of a dash.
The answer is in the JSX section of the Typescript Handbook:
If an attribute name is not a valid JS identifier (like a
data-*
attribute), it is not considered to be an error if it is not found in the element attributes type.
The entire section on JSX is a very enlightening read.
I'm afraid the answer to the "how to define types like this in TypeScript" question is... We don't. JSX support is built into the compiler. There's plenty of interesting things we can customize, however.
The TS handbook note in Fabio's answer explained all of this, just want to expand a little bit. In short, kebab-case attributes are not considered valid by TS but will not throw error; but attribute prefixed by data-
or aria-
are considered valid.
React (since 16) accepts custom attributes, i.e <div foo />
and <div whateverYouLike={2}>
should work.
What I find confusing with React, is that data-*
and aria-*
should be written as-is, vs. converting them to camelCase like everything else. Especially when these attributes are converted into camelCase in vanilla DOM:
<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"
There're no reasons ever given for this, so we can only speculate. Perhaps something to do with a11y toolings, parsing convenience, etc.
The reasons that <div foo />
throws in TS is because TS provides a strict set of valid property names. However, as noted by the other answer, TS will not throw error on random-foo
because it is considered an invalid JS identifier. My speculation is because DOM elements allow arbitrary properties, so this is a compromise that allow correct typing in most cases in TS but provide some sort of escape hatch. Would love to know the reasons behind these decisions.
How to define types like this in TypeScript? i.e. Only allowing standard or kebab-case attributes.
As Fabio has already pointed out, JSX support is built into the compiler. However, beside the ability to identify what constitute a valid attribute name, I don't think there's a lot of magic to it: there's a comprehensive list of valid DOM attributes. TS doesn't throw error if you mix kebab & camel cases, i.e <div data-myName>
work, <div myName/>
doesn't, etc., so it does not differ by casing either.
If you know all your valid props in advance, you can emulate the same thing.
// allow only these prop names, which happened to be all camelCased
interface MyThing {
name: string
myName: string
anotherProp: string
}
In case of kebab-case, template literal types could be helpful:
type ValidPrefix = "data" | "aria";
type ValidSuffix = "banana" | "apple" | "pear";
type ComputedProps = {
[key in `${ValidPrefix}-${ValidSuffix}`]?: string;
};
const x: ComputedProps = {
"data-apple": 'hi'
};
Beyond this, there's currently no mechanism in TS that can differ between camelCase & kebab-case string.
If you're looking for a way to augment JSX to allow custom props and custom elements, this is a way to do it:
Augmenting JSX attribute to allow custom props & custom elements
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With