alexnault / classix

🏛️ The fastest and tiniest utility for conditionally joining classNames.
https://www.npmjs.com/package/classix
MIT License
198 stars 4 forks source link

types: improve type inference for cx concatenation #64

Open iddar opened 1 week ago

iddar commented 1 week ago

Improved Type Inference for cx Function

This PR enhances the type system for the cx function to provide more accurate type literals in the return type without affecting runtime performance.

image

Changes

Type System Improvements

Before

cx("foo", "bar")        // type: string
cx("foo", null, "bar")  // type: never

After

cx("foo", "bar")        // type: "foo bar"
cx("foo", null, "bar")  // type: "foo bar"
cx("a", "b", "c")       // type: "a b c"
// Conditional types are preserved
const test5 = cx("foo", false ? "bar" : "baz") satisfies "foo bar" | "foo baz";

Implementation

The solution uses TypeScript's type system to:

  1. Filter valid string arguments from the input tuple
  2. Join the filtered strings with spaces
  3. Preserve conditional types and unions
type FilterStrings<T extends readonly any[]> = T extends readonly [infer F, ...infer R]
  ? F extends string
    ? [F, ...FilterStrings<R>]
    : FilterStrings<R>
  : [];

type JoinStrings<T extends readonly string[]> = T extends readonly [infer F, ...infer R]
  ? F extends string
    ? R extends readonly string[]
      ? R['length'] extends 0
        ? F
        : `${F} ${JoinStrings<R>}`
      : F
    : never
  : '';

Justification

Notes


cx(...["foo", "bar"]) Considered but not included

While analyzing type improvements, we found an edge case with spread arrays:

cx(...["foo", "bar"]) // Currently types as: `${string} ${string}`

when the array is statically known the workaround is to use the as const assertion:

cx(...["foo", "bar"] as const) // types as: "foo bar"

Same situation for object properties:

const obj = {  b: "bar" };
cx("foo, obj.b) // Currently types as: `${string} ${string}`
// fix
const obj = {  b: "bar" } as const;
cx("foo", obj.b) // types as: "foo bar"

We decided not to cover this case because:

  1. Type System Complexity: Adding support for spread arrays would significantly increase type system complexity.

  2. Common Usage: This pattern is rarely used in practice, as most calls to cx use direct string literals or variables.

Please let me know if you'd like me to explain any part of the implementation in more detail.

alexnault commented 1 week ago

Hey @iddar, thanks for the PR! I really appreciate it.

While I think this is an interesting feature, I'm debating adding it for two reasons :

image

Also, your "Before" example shows:

cx("foo", null, "bar")  // type: never

I tried it and I'm getting:

cx("foo", null, "bar")  // type: string

Which is the expected behavior.


Again, I'm not against the idea, I'm just on the fence. So let me know what you think!

Cheers!