Skip to content

Using clsx incorporating class declaration with tailwind-merge causes error #14874

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
mpost opened this issue Jan 2, 2025 · 2 comments
Open

Comments

@mpost
Copy link

mpost commented Jan 2, 2025

Describe the bug

Combining the latest clsx supporting HTMLAttributes with tailwind-merge creates type incompatibility.

HTMLAttributes declared by svelte

export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, DOMAttributes<T> {
	class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
	...
}

Expected type from tailwind-merge

export type ClassNameValue = ClassNameArray | string | 0 | 0n | false | undefined | null

Error Logs

Argument of type 'string | ClassArray | ClassDictionary | null | undefined' is not assignable to parameter of type 'ClassNameValue'.
  Type 'ClassArray' is not assignable to type 'ClassNameValue'.
    Type 'ClassValue[]' is not assignable to type 'ClassNameArray'.
      Type 'ClassValue' is not assignable to type 'ClassNameValue'.
        Type 'number' is not assignable to type 'ClassNameValue'. (ts)
          : 'size-12',
    cssClass,
  )}

Reproduction

<script lang="ts">
  import { twMerge } from 'tailwind-merge'
  import type { HTMLButtonAttributes } from 'svelte/elements'
  import type { Snippet } from 'svelte'

  interface Props extends HTMLButtonAttributes {
    children?: Snippet
  }

  let { class: cssClass, children, ...rest }: Props = $props()
</script>

<button {...rest} class={twMerge('rounded', cssClass)}>
  {@render children?.()}
</button>

System Info

npmPackages:
    svelte: ^5.16.0
    tailwind-merge: ^2.6.0

Severity

blocking an upgrade

Hacky workaround

One possibility is to redefine the class property as a common type (eg a simple string).

  interface Props extends HTMLButtonAttributes {
    children?: Snippet
    class?: string
  }
@0xMurage
Copy link

It goes beyond simple type incompatibility.

Since Svelte 5.16, the class prop can be an object or array and is converted to a string using clsx. If a parent component passes a class as an object, the child component will receive it as an object.

For example, if one calls the component you shared with class prop of type object:

<CustomButton class={{ 'bg-orange-500': someCondition }}>
  Oh dear! 
</CustomButton>

This creates a problem because the received type is not a string but rather a clsx class dictionary.

A workaround is to preprocess class props with clsx before passing it to tailwind-merge.

<script lang="ts">
  import { twMerge } from 'tailwind-merge'
  import { clsx } from 'clsx';
  import type { HTMLButtonAttributes } from 'svelte/elements'

  let  props:HTMLButtonAttributes = $props()

</script>

<button {...rest} class={twMerge('rounded', clsx(props.class))}>
  {@render children?.()}
</button>

Svelte should probably expose the clsx function (or functionality) so that even if there is an internal change of the behavior, there will be minimal negative impact to user code.

@h4ckedneko
Copy link

Another hacky workaround is to type assert the props.class as string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants