Skip to content

Commit 34674ae

Browse files
itsdougeskylorhall-atlassian
andauthoredDec 18, 2023
Fix XCSSProp types (#1596)
* fix: resolve cssMap from strict api having its generic output type unset * chore: add more tests * chore: changeset * chore: fix test * Update expert error location in test Co-authored-by: Kylor Hall <136543114+kylorhall-atlassian@users.noreply.github.com> * chore: fix comment --------- Co-authored-by: Kylor Hall <136543114+kylorhall-atlassian@users.noreply.github.com>
1 parent e2d1e4d commit 34674ae

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed
 

‎.changeset/strong-oranges-turn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/react': patch
3+
---
4+
5+
Fix `cssMap` returned from `createStrictAPI` to return types based on the generic input, fixing usage with the `XCSSProp` API.

‎packages/react/src/create-strict-api/__tests__/index.test.tsx

+140
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,146 @@ describe('createStrictAPI()', () => {
271271
});
272272

273273
describe('XCSSProp', () => {
274+
it('should allow valid values from cssMap', () => {
275+
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', never>> }) {
276+
return <button data-testid="button" className={xcss} />;
277+
}
278+
279+
const styles = cssMap({ bg: { background: 'var(--ds-surface)' } });
280+
const { getByTestId } = render(<Button xcss={styles.bg} />);
281+
282+
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
283+
});
284+
285+
it('should disallow invalid values from cssMap', () => {
286+
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', never>> }) {
287+
return <button data-testid="button" className={xcss} />;
288+
}
289+
290+
const styles = cssMap({ bg: { accentColor: 'red' } });
291+
const { getByTestId } = render(
292+
<Button
293+
// @ts-expect-error — Type 'CompiledStyles<{ accentColor: "red"; }>' is not assignable to type ...
294+
xcss={styles.bg}
295+
/>
296+
);
297+
298+
expect(getByTestId('button')).toHaveCompiledCss('accent-color', 'red');
299+
});
300+
301+
it('should allow constrained background and pseudo', () => {
302+
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', '&:hover'>> }) {
303+
return <button data-testid="button" className={xcss} />;
304+
}
305+
306+
const styles = cssMap({
307+
primary: {
308+
background: 'var(--ds-surface)',
309+
'&:hover': { background: 'var(--ds-surface-hover)' },
310+
},
311+
});
312+
313+
const { getByTestId } = render(<Button xcss={styles.primary} />);
314+
315+
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
316+
});
317+
318+
it('should type error on a partially invalid declaration', () => {
319+
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', '&:hover'>> }) {
320+
return <button data-testid="button" className={xcss} />;
321+
}
322+
323+
const styles = cssMap({
324+
bad: {
325+
// @ts-expect-error — Property 'bad' is incompatible with index signature.
326+
foo: 'bar',
327+
color: 'var(--ds-text)',
328+
},
329+
});
330+
331+
const { getByTestId } = render(
332+
<Button
333+
// @ts-expect-error — Type 'CompiledStyles<{ foo: string; color: "var(--ds-text)"; }>' is not assignable to type
334+
xcss={styles.bad}
335+
/>
336+
);
337+
338+
expect(getByTestId('button')).toHaveCompiledCss('color', 'var(--ds-text)');
339+
});
340+
341+
it('should error with values not in the strict `CompiledAPI`', () => {
342+
function Button({
343+
xcss,
344+
}: {
345+
xcss: ReturnType<typeof XCSSProp<'background' | 'color', '&:hover'>>;
346+
}) {
347+
return <button data-testid="button" className={xcss} />;
348+
}
349+
350+
const styles = cssMap({
351+
primary: {
352+
// @ts-expect-error -- This is not in the `createStrictAPI` schema—this should be a css variable.
353+
color: 'red',
354+
background: 'var(--ds-surface)',
355+
'&:hover': { background: 'var(--ds-surface-hover)' },
356+
},
357+
});
358+
359+
const { getByTestId } = render(
360+
<Button
361+
// @ts-expect-error -- Errors because `color` conflicts with the `XCSSProp` schema–`color` should be a css variable.
362+
xcss={styles.primary}
363+
/>
364+
);
365+
366+
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
367+
});
368+
369+
it('should error with properties not in the `XCSSProp`', () => {
370+
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'color', '&:focus'>> }) {
371+
return <button data-testid="button" className={xcss} />;
372+
}
373+
374+
const styles = cssMap({
375+
primary: {
376+
background: 'var(--ds-surface)',
377+
'&:hover': { background: 'var(--ds-surface-hover)' },
378+
},
379+
});
380+
381+
const { getByTestId } = render(
382+
<Button
383+
// @ts-expect-error -- Errors because `background` + `&:hover` are not in the `XCSSProp` schema.
384+
xcss={styles.primary}
385+
/>
386+
);
387+
388+
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
389+
});
390+
391+
it('should error with invalid values', () => {
392+
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', '&:hover'>> }) {
393+
return <button data-testid="button" className={xcss} />;
394+
}
395+
396+
const styles = cssMap({
397+
primary: {
398+
// @ts-expect-error -- Fails because `foo` is not assignable to our CSSProperties whatsoever.
399+
foo: 'bar',
400+
background: 'var(--ds-surface)',
401+
'&:hover': {
402+
// This does not fail, but would if the above was removed; this should be tested in raw `cssMap` fully.
403+
foo: 'bar',
404+
background: 'var(--ds-surface-hover)',
405+
},
406+
},
407+
});
408+
409+
const { getByTestId } = render(<Button xcss={styles.primary} />);
410+
411+
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
412+
});
413+
274414
it('should allow valid values', () => {
275415
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', never>> }) {
276416
return <button data-testid="button" className={xcss} />;

‎packages/react/src/create-strict-api/index.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type PickObjects<TObject> = {
2121
type CSSStyles<TSchema extends CompiledSchema> = StrictCSSProperties &
2222
PseudosDeclarations &
2323
EnforceSchema<TSchema>;
24+
2425
type CSSMapStyles<TSchema extends CompiledSchema> = Record<string, CSSStyles<TSchema>>;
2526

2627
interface CompiledAPI<TSchema extends CompiledSchema> {
@@ -57,9 +58,10 @@ interface CompiledAPI<TSchema extends CompiledSchema> {
5758
* ```
5859
*/
5960
cssMap<TStylesMap extends CSSMapStyles<TSchema>>(
60-
// NOTE: This should match the generic `TStylesMap extends …` as we want this arg to strictly satisfy this type, not just extend it.
61-
// The "extends" functionality is to infer and build the return type, this is to enforce the input type.
62-
styles: CSSMapStyles<TSchema>
61+
// We intersection type the generic both with the concrete type and the generic to ensure the output has the generic applied.
62+
// Without both it would either have the input arg not have excess property check kick in allowing unexpected values or
63+
// have all values set as the output making usage with XCSSProp have type violations unexpectedly.
64+
styles: CSSMapStyles<TSchema> & TStylesMap
6365
): {
6466
readonly [P in keyof TStylesMap]: CompiledStyles<TStylesMap[P]>;
6567
};

0 commit comments

Comments
 (0)
Please sign in to comment.