types: improve type inference for cx concatenation
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.
Changes
- Added
FilterStringstype helper to process tuple types - Added
JoinStringstype helper to generate exact string literal types - Updated function signature to preserve string literals
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:
- Filter valid string arguments from the input tuple
- Join the filtered strings with spaces
- 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
- Improves DX by providing exact type information
- Helps catch type errors at compile time
- No runtime performance impact (types are erased during compilation)
- Better IDE support with accurate autocompletion
Notes
- This is a type-only change
- No runtime behavior modifications
- Fully backward compatible
- No bundle size impact
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:
-
Type System Complexity: Adding support for spread arrays would significantly increase type system complexity.
-
Common Usage: This pattern is rarely used in practice, as most calls to
cxuse direct string literals or variables.
Please let me know if you'd like me to explain any part of the implementation in more detail.