Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why kebab-case non-standard attributes are allowed while others aren't? And how to define types like this in TypeScript?

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.

like image 867
Wenfang Du Avatar asked May 24 '21 14:05

Wenfang Du


People also ask

What is kebab case in English grammar?

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”

What is the difference between snake case and 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.

What is kebab case in Seo?

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.

What is a scream kebab in JavaScript?

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.


2 Answers

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.

like image 177
Fábio Batista Avatar answered Oct 20 '22 04:10

Fábio Batista


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

like image 10
Derek Nguyen Avatar answered Oct 20 '22 03:10

Derek Nguyen