Skip to content

Commit 5701b91

Browse files
authoredMar 6, 2024··
Refactor strict API types (#1636)
* chore: refactor strict types * fix: resolve type errors * chore: refactor xcss prop types to use apply schema type * fix: fix type errors * fix: style types * chore: revert * chore: document types * chore: changeset * fix: compiled styles allowing passing invalid styles * fix: too many unions * fix: type error with css func and xss prop * fix: type violation * chore: fix test * chore: add extra tests * chore: resolve code review comments * chore: update test file
1 parent 0ae6052 commit 5701b91

File tree

11 files changed

+446
-93
lines changed

11 files changed

+446
-93
lines changed
 

‎.changeset/tricky-poems-greet.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@compiled/react': minor
3+
---
4+
5+
Types for `createStrictAPI` have been refactored to improve type inference and expectations.
6+
7+
Previously defining the schema came with a lot of redundant work. For every pseudo that you wanted to type you would have to define it, and then all of the base types again, like so:
8+
9+
```ts
10+
interface Schema {
11+
background: 'var(--bg)';
12+
color: 'var(--color)';
13+
'&:hover': {
14+
background: 'var(--bg)';
15+
color: 'var(--color-hovered)';
16+
};
17+
}
18+
19+
createStrictAPI<Schema>();
20+
```
21+
22+
If you missed a value / didn't type every possible pseudo it would fallback to the CSSProperties value from csstype. This was mostly unexpected. So for example right now `&:hover` has been typed, but no other pseudo... meaning no other pseudos would benefit from the schema types!
23+
24+
With this refactor all CSS properties use the top types unless a more specific one is defined, meaning you only need to type the values you want to explicitly support. In the previous example we're now able to remove the `background` property as it's the same as the top one.
25+
26+
```diff
27+
interface Schema {
28+
background: 'var(--bg)';
29+
color: 'var(--color)';
30+
'&:hover': {
31+
- background: 'var(--bg)';
32+
color: 'var(--color-hovered)';
33+
};
34+
}
35+
36+
createStrictAPI<Schema>();
37+
```

‎packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api-recursive.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,29 @@ type ColorPressed = 'var(--ds-text-pressed)';
66
type Background = 'var(--ds-bold)' | 'var(--ds-success)';
77
type BackgroundHovered = 'var(--ds-bold-hovered)' | 'var(--ds-success-hovered)';
88
type BackgroundPressed = 'var(--ds-bold-pressed)' | 'var(--ds-success-pressed)';
9+
type Space = 'var(--ds-space-050)' | 'var(--ds-space-0)';
910

1011
interface Properties {
1112
color: Color;
1213
backgroundColor: Background;
14+
padding: Space;
1315
}
1416

15-
interface HoveredProperties extends Omit<Properties, 'backgroundColor' | 'color'> {
17+
interface HoveredProperties {
1618
color: ColorHovered;
1719
backgroundColor: BackgroundHovered;
1820
}
1921

20-
interface PressedProperties extends Omit<Properties, 'backgroundColor' | 'color'> {
22+
interface PressedProperties {
2123
color: ColorPressed;
2224
backgroundColor: BackgroundPressed;
2325
}
2426

25-
interface StrictAPI extends Properties {
27+
interface CSSPropertiesSchema extends Properties {
2628
'&:hover': HoveredProperties;
2729
'&:active': PressedProperties;
28-
'&::before': Properties;
29-
'&::after': Properties;
3030
}
3131

32-
const { css, XCSSProp, cssMap, cx } = createStrictAPI<StrictAPI>();
32+
const { css, cssMap, cx, XCSSProp } = createStrictAPI<CSSPropertiesSchema>();
3333

3434
export { css, XCSSProp, cssMap, cx };
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { createStrictAPI } from '../../index';
22

3-
const { css, XCSSProp, cssMap, cx } = createStrictAPI<{
3+
interface CSSPropertiesSchema {
44
'&:hover': {
55
color: 'var(--ds-text-hover)';
66
background: 'var(--ds-surface-hover)' | 'var(--ds-surface-sunken-hover)';
77
};
8-
color: 'var(--ds-text)';
8+
color: 'var(--ds-text)' | 'var(--ds-text-bold)';
99
background: 'var(--ds-surface)' | 'var(--ds-surface-sunken)';
1010
bkgrnd: 'red' | 'green';
11-
}>();
11+
}
12+
13+
const { css, XCSSProp, cssMap, cx } = createStrictAPI<CSSPropertiesSchema>();
1214

1315
export { css, XCSSProp, cssMap, cx };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/** @jsxImportSource @compiled/react */
2+
// eslint-disable-next-line import/no-extraneous-dependencies
3+
import { css } from '@compiled/react';
4+
import type { XCSSProp } from '@fixture/strict-api-test';
5+
import { css as strictCSS } from '@fixture/strict-api-test';
6+
import { render } from '@testing-library/react';
7+
8+
function Button({
9+
xcss,
10+
testId,
11+
}: {
12+
testId: string;
13+
xcss: ReturnType<typeof XCSSProp<'backgroundColor', '&:hover'>>;
14+
}) {
15+
return <button data-testid={testId} className={xcss} />;
16+
}
17+
18+
describe('css func from strict api', () => {
19+
it('should type error when passing css func result into xcss prop', () => {
20+
const strictStyles = strictCSS({
21+
backgroundColor: 'var(--ds-surface)',
22+
});
23+
const looseStyles = css({
24+
backgroundColor: 'red',
25+
});
26+
27+
const { getByTestId } = render(
28+
<>
29+
<Button
30+
testId="button-1"
31+
// @ts-expect-error — CSS func currently doesn't work with XCSS prop so should type error
32+
xcss={strictStyles}
33+
/>
34+
<Button
35+
testId="button-2"
36+
// @ts-expect-error — CSS func currently doesn't work with XCSS prop so should type error
37+
xcss={looseStyles}
38+
/>
39+
<div data-testid="div-1" css={[strictStyles, looseStyles]} />
40+
<div data-testid="div-2" css={[looseStyles, strictStyles]} />
41+
</>
42+
);
43+
44+
expect(getByTestId('button-1').className).toEqual('');
45+
expect(getByTestId('button-2').className).toEqual('');
46+
expect(getByTestId('div-1')).toHaveCompiledCss('background-color', 'red');
47+
expect(getByTestId('div-2')).toHaveCompiledCss('background-color', 'var(--ds-surface)');
48+
});
49+
});

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

+26-5
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('createStrictAPI()', () => {
9999
const styles = cssMap({
100100
primary: {
101101
// @ts-expect-error — Type '""' is not assignable to type ...
102-
color: 's',
102+
color: '',
103103
// @ts-expect-error — Type '""' is not assignable to type ...
104104
backgroundColor: '',
105105
'&:hover': {
@@ -155,15 +155,15 @@ describe('createStrictAPI()', () => {
155155
backgroundColor: '',
156156
'&:hover': {
157157
// @ts-expect-error — Type '""' is not assignable to type ...
158-
color: '',
158+
color: 'var(--ds-text)',
159159
// @ts-expect-error — Type '""' is not assignable to type ...
160-
backgroundColor: '',
160+
backgroundColor: 'var(--ds-success)',
161161
},
162162
'&:active': {
163163
// @ts-expect-error — Type '""' is not assignable to type ...
164-
color: '',
164+
color: 'var(--ds-text)',
165165
// @ts-expect-error — Type '""' is not assignable to type ...
166-
backgroundColor: '',
166+
backgroundColor: 'var(--ds-success)',
167167
},
168168
'&::before': {
169169
// @ts-expect-error — Type '""' is not assignable to type ...
@@ -188,21 +188,31 @@ describe('createStrictAPI()', () => {
188188
describe('type success', () => {
189189
it('should pass type check for css()', () => {
190190
const styles = css({
191+
// @ts-expect-error — should be a value from the schema
192+
padding: '10px',
191193
color: 'var(--ds-text)',
192194
backgroundColor: 'var(--ds-bold)',
193195
'&:hover': {
196+
// @ts-expect-error — should be a value from the schema
197+
padding: '10px',
194198
color: 'var(--ds-text-hovered)',
195199
backgroundColor: 'var(--ds-bold-hovered)',
196200
},
197201
'&:active': {
202+
// @ts-expect-error — should be a value from the schema
203+
padding: '10px',
198204
color: 'var(--ds-text-pressed)',
199205
backgroundColor: 'var(--ds-bold-pressed)',
200206
},
201207
'&::before': {
208+
// @ts-expect-error — should be a value from the schema
209+
padding: '10px',
202210
color: 'var(--ds-text)',
203211
backgroundColor: 'var(--ds-bold)',
204212
},
205213
'&::after': {
214+
// @ts-expect-error — should be a value from the schema
215+
padding: '10px',
206216
color: 'var(--ds-text)',
207217
backgroundColor: 'var(--ds-bold)',
208218
},
@@ -218,19 +228,30 @@ describe('createStrictAPI()', () => {
218228
primary: {
219229
color: 'var(--ds-text)',
220230
backgroundColor: 'var(--ds-bold)',
231+
// @ts-expect-error — should be a value from the schema
232+
padding: '10px',
221233
'&:hover': {
234+
accentColor: 'red',
235+
// @ts-expect-error — should be a value from the schema
236+
padding: '10px',
222237
color: 'var(--ds-text-hovered)',
223238
backgroundColor: 'var(--ds-bold-hovered)',
224239
},
225240
'&:active': {
241+
// @ts-expect-error — should be a value from the schema
242+
padding: '10px',
226243
color: 'var(--ds-text-pressed)',
227244
backgroundColor: 'var(--ds-bold-pressed)',
228245
},
229246
'&::before': {
247+
// @ts-expect-error — should be a value from the schema
248+
padding: '10px',
230249
color: 'var(--ds-text)',
231250
backgroundColor: 'var(--ds-bold)',
232251
},
233252
'&::after': {
253+
// @ts-expect-error — should be a value from the schema
254+
padding: '10px',
234255
color: 'var(--ds-text)',
235256
backgroundColor: 'var(--ds-bold)',
236257
},

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

+211-28
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
/** @jsxImportSource @compiled/react */
2+
// eslint-disable-next-line import/no-extraneous-dependencies
3+
import { cssMap as cssMapLoose } from '@compiled/react';
24
import { render } from '@testing-library/react';
35

4-
import { css, cssMap, XCSSProp } from './__fixtures__/strict-api';
6+
import { css, cssMap, XCSSProp, cx } from './__fixtures__/strict-api';
57

68
describe('createStrictAPI()', () => {
79
describe('css()', () => {
810
it('should type error when circumventing the excess property check', () => {
9-
const styles = css({
11+
const stylesOne = css({
1012
color: 'var(--ds-text)',
1113
accentColor: 'red',
1214
// @ts-expect-error — Type 'string' is not assignable to type 'undefined'.ts(2322)
1315
bkgrnd: 'red',
16+
'&:hover': {
17+
color: 'var(--ds-text-hover)',
18+
},
19+
});
20+
const stylesTwo = css({
21+
color: 'var(--ds-text)',
22+
accentColor: 'red',
1423
'&:hover': {
1524
color: 'var(--ds-text-hover)',
1625
// @ts-expect-error — Type 'string' is not assignable to type 'undefined'.ts(2322)
1726
bkgrnd: 'red',
1827
},
1928
});
2029

21-
const { getByTestId } = render(<div css={styles} data-testid="div" />);
30+
const { getByTestId } = render(<div css={[stylesOne, stylesTwo]} data-testid="div" />);
2231

2332
expect(getByTestId('div')).toHaveCompiledCss('color', 'var(--ds-text)');
2433
});
@@ -79,7 +88,7 @@ describe('createStrictAPI()', () => {
7988
color: 'var(--ds-text)',
8089
all: 'inherit',
8190
'&:hover': { color: 'var(--ds-text-hover)' },
82-
'&:invalid': { color: 'orange' },
91+
'&:invalid': { color: 'var(--ds-text)' },
8392
});
8493

8594
const { getByTestId } = render(<div css={styles} data-testid="div" />);
@@ -146,7 +155,7 @@ describe('createStrictAPI()', () => {
146155
accentColor: 'red',
147156
all: 'inherit',
148157
'&:hover': { color: 'var(--ds-text-hover)' },
149-
'&:invalid': { color: 'orange' },
158+
'&:invalid': { color: 'var(--ds-text)' },
150159
},
151160
});
152161

@@ -271,12 +280,103 @@ describe('createStrictAPI()', () => {
271280
});
272281

273282
describe('XCSSProp', () => {
283+
it('should error with values not in the strict `CompiledStrictSchema`', () => {
284+
function Button({
285+
xcss,
286+
testId,
287+
}: {
288+
testId: string;
289+
xcss: ReturnType<typeof XCSSProp<'background' | 'color', '&:hover'>>;
290+
}) {
291+
return <button data-testid={testId} className={xcss} />;
292+
}
293+
// NOTE: For some reason the "background" property is being expanded to "string" instead of
294+
// staying narrowed as "var(--ds-surface-hover)" meaning it breaks when used with the strict
295+
// schema loaded XCSS prop. This is a bug and unexpected.
296+
const stylesValidRoot = cssMapLoose({
297+
primary: {
298+
color: 'var(--ds-text)',
299+
'&:hover': { color: 'var(--ds-text-hover)', background: 'var(--ds-surface-hover)' },
300+
},
301+
});
302+
const stylesInvalidRoot = cssMapLoose({
303+
primary: {
304+
color: 'red',
305+
'&:hover': { color: 'var(--ds-text-hover)', background: 'var(--ds-surface-hover)' },
306+
},
307+
});
308+
const stylesInvalid = cssMap({
309+
primary: {
310+
// @ts-expect-error -- This is not valid in the CompiledStrictSchema
311+
color: 'red',
312+
'&:hover': { color: 'var(--ds-text-hover)', background: 'var(--ds-surface-hover)' },
313+
},
314+
});
315+
316+
const stylesValid = cssMap({
317+
primary: {
318+
color: 'var(--ds-text)',
319+
'&:hover': { color: 'var(--ds-text-hover)', background: 'var(--ds-surface-hover)' },
320+
},
321+
});
322+
323+
const { getByTestId } = render(
324+
<>
325+
<Button
326+
testId="button-invalid-root"
327+
// @ts-expect-error — This conflicts with the custom API, should be a different bg color
328+
xcss={stylesInvalidRoot.primary}
329+
/>
330+
<Button
331+
testId="button-invalid-root-cx"
332+
// @ts-expect-error — This conflicts with the custom API, should be a different bg color
333+
xcss={cx(stylesInvalidRoot.primary, stylesValid.primary)}
334+
/>
335+
<Button
336+
testId="button-valid-root"
337+
// @ts-expect-error — For some reason the "background" property is being expanded to "string" instead of
338+
// staying narrowed as "var(--ds-surface-hover)" meaning it breaks when used with the strict
339+
// schema loaded XCSS prop. This is a bug and unexpected.
340+
xcss={stylesValidRoot.primary}
341+
/>
342+
<Button
343+
testId="button-valid-root-cx"
344+
// @ts-expect-error — For some reason the "background" property is being expanded to "string" instead of
345+
// staying narrowed as "var(--ds-surface-hover)" meaning it breaks when used with the strict
346+
// schema loaded XCSS prop. This is a bug and unexpected.
347+
xcss={cx(stylesValidRoot.primary, stylesValid.primary)}
348+
/>
349+
<Button
350+
testId="button-invalid-strict"
351+
// @ts-expect-error — TODO: This should conflict, but when `cssMap` conflicts, it gets a different type (this has `ApplySchema`, not the raw object), so this doesn't error? Weird…
352+
xcss={stylesInvalid.primary}
353+
/>
354+
<Button
355+
testId="button-invalid-strict-cx"
356+
// @ts-expect-error — TODO: This should conflict, but when `cssMap` conflicts, it gets a different type (this has `ApplySchema`, not the raw object), so this doesn't error? Weird…
357+
xcss={cx(stylesInvalid.primary, stylesValid.primary)}
358+
/>
359+
<Button testId="button-valid" xcss={stylesValid.primary} />
360+
<Button testId="button-valid-cx" xcss={cx(stylesValid.primary, stylesValid.primary)} />
361+
<Button
362+
testId="button-invalid-direct"
363+
xcss={{
364+
// @ts-expect-error — This is not in the `createStrictAPI` schema—this should be a css variable.
365+
color: 'red',
366+
}}
367+
/>
368+
</>
369+
);
370+
371+
expect(getByTestId('button-invalid-root')).toHaveCompiledCss('color', 'red');
372+
});
373+
274374
it('should allow valid values from cssMap', () => {
275375
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', never>> }) {
276376
return <button data-testid="button" className={xcss} />;
277377
}
278-
279378
const styles = cssMap({ bg: { background: 'var(--ds-surface)' } });
379+
280380
const { getByTestId } = render(<Button xcss={styles.bg} />);
281381

282382
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
@@ -302,7 +402,6 @@ describe('createStrictAPI()', () => {
302402
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', '&:hover'>> }) {
303403
return <button data-testid="button" className={xcss} />;
304404
}
305-
306405
const styles = cssMap({
307406
primary: {
308407
background: 'var(--ds-surface)',
@@ -341,29 +440,53 @@ describe('createStrictAPI()', () => {
341440
it('should error with values not in the strict `CompiledAPI`', () => {
342441
function Button({
343442
xcss,
443+
testId,
344444
}: {
445+
testId: string;
345446
xcss: ReturnType<typeof XCSSProp<'background' | 'color', '&:hover'>>;
346447
}) {
347-
return <button data-testid="button" className={xcss} />;
448+
return <button data-testid={testId} className={xcss} />;
348449
}
349450

350-
const styles = cssMap({
451+
const stylesOne = cssMapLoose({
351452
primary: {
352-
// @ts-expect-error -- This is not in the `createStrictAPI` schema—this should be a css variable.
453+
color: 'red',
454+
background: 'var(--ds-surface)',
455+
'&:hover': { background: 'var(--ds-surface-hover)' },
456+
},
457+
});
458+
const stylesTwo = cssMap({
459+
primary: {
460+
// @ts-expect-error — This is not in the `createStrictAPI` schema—this should be a css variable.
353461
color: 'red',
354462
background: 'var(--ds-surface)',
355463
'&:hover': { background: 'var(--ds-surface-hover)' },
356464
},
357465
});
358466

359467
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-
/>
468+
<>
469+
<Button
470+
testId="button-1"
471+
// @ts-expect-error — This is not in the `createStrictAPI` schema—this should be a css variable.
472+
xcss={stylesOne.primary}
473+
/>
474+
<Button
475+
testId="button-3"
476+
// @ts-expect-error — This is not in the `createStrictAPI` schema—this should be a css variable.
477+
xcss={stylesTwo.stylesTwo}
478+
/>
479+
<Button
480+
testId="button-2"
481+
xcss={{
482+
// @ts-expect-error — This is not in the `createStrictAPI` schema—this should be a css variable.
483+
color: 'red',
484+
}}
485+
/>
486+
</>
364487
);
365488

366-
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
489+
expect(getByTestId('button-1')).toHaveCompiledCss('background', 'var(--ds-surface)');
367490
});
368491

369492
it('should error with properties not in the `XCSSProp`', () => {
@@ -380,7 +503,7 @@ describe('createStrictAPI()', () => {
380503

381504
const { getByTestId } = render(
382505
<Button
383-
// @ts-expect-error -- Errors because `background` + `&:hover` are not in the `XCSSProp` schema.
506+
// @ts-expect-error Errors because `background` + `&:hover` are not in the `XCSSProp` schema.
384507
xcss={styles.primary}
385508
/>
386509
);
@@ -392,21 +515,24 @@ describe('createStrictAPI()', () => {
392515
function Button({ xcss }: { xcss: ReturnType<typeof XCSSProp<'background', '&:hover'>> }) {
393516
return <button data-testid="button" className={xcss} />;
394517
}
395-
396-
const styles = cssMap({
518+
const stylesOne = cssMap({
397519
primary: {
398-
// @ts-expect-error -- Fails because `foo` is not assignable to our CSSProperties whatsoever.
520+
// @ts-expect-error Fails because `foo` is not assignable to our CSSProperties whatsoever.
399521
foo: 'bar',
400522
background: 'var(--ds-surface)',
523+
},
524+
});
525+
const stylesTwo = cssMap({
526+
hover: {
401527
'&:hover': {
402-
// This does not fail, but would if the above was removed; this should be tested in raw `cssMap` fully.
528+
// @ts-expect-error — Fails because `foo` is not assignable to our CSSProperties whatsoever.
403529
foo: 'bar',
404530
background: 'var(--ds-surface-hover)',
405531
},
406532
},
407533
});
408534

409-
const { getByTestId } = render(<Button xcss={styles.primary} />);
535+
const { getByTestId } = render(<Button xcss={cx(stylesOne.primary, stylesTwo.hover)} />);
410536

411537
expect(getByTestId('button')).toHaveCompiledCss('background', 'var(--ds-surface)');
412538
});
@@ -536,27 +662,84 @@ describe('createStrictAPI()', () => {
536662

537663
it('should enforce required properties', () => {
538664
function Button({
665+
testId,
539666
xcss,
540667
}: {
668+
testId: string;
541669
xcss: ReturnType<
542670
typeof XCSSProp<
543-
'background',
671+
'background' | 'color',
544672
never,
545673
{ requiredProperties: 'background'; requiredPseudos: never }
546674
>
547675
>;
548676
}) {
549-
return <button data-testid="button" className={xcss} />;
677+
return <button data-testid={`button-${testId}`} className={xcss} />;
550678
}
551679

680+
const stylesValid = cssMap({
681+
primary: { background: 'var(--ds-surface)' },
682+
});
683+
const stylesInvalid = cssMap({
684+
primary: { color: 'var(--ds-text)' },
685+
});
686+
552687
const { getByTestId } = render(
553-
<Button
554-
// @ts-expect-error — Type '{}' is not assignable to type 'Internal$XCSSProp<"background", never, EnforceSchema<{ background: "var(--ds-surface)" | "var(--ds-surface-sunken"; }>, object, { requiredProperties: "background"; requiredPseudos: never; }>'.ts(2322)
555-
xcss={{}}
556-
/>
688+
<>
689+
<Button testId="valid" xcss={stylesValid.primary} />
690+
<Button
691+
testId="invalid"
692+
// @ts-expect-error — This is not assignable as it's missing the required `background` property.
693+
xcss={stylesInvalid.primary}
694+
/>
695+
</>
557696
);
558697

559-
expect(getByTestId('button')).not.toHaveCompiledCss('color', 'red');
698+
expect(getByTestId('button-valid')).toHaveCompiledCss('background', 'var(--ds-surface)');
699+
expect(getByTestId('button-invalid')).toHaveCompiledCss('color', 'var(--ds-text)');
700+
});
701+
702+
it('should enforce required psuedos', () => {
703+
function Button({
704+
testId,
705+
xcss,
706+
}: {
707+
testId: string;
708+
xcss: ReturnType<
709+
typeof XCSSProp<
710+
'color',
711+
'&:hover' | '&:focus',
712+
{ requiredProperties: never; requiredPseudos: '&:hover' }
713+
>
714+
>;
715+
}) {
716+
return <button data-testid={`button-${testId}`} className={xcss} />;
717+
}
718+
719+
const stylesValid = cssMap({
720+
primary: { '&:hover': { color: 'var(--ds-text-hover)' } },
721+
});
722+
const stylesInvalid = cssMap({
723+
primary: { '&:focus': { color: 'var(--ds-text)' } },
724+
});
725+
726+
const { getByTestId } = render(
727+
<>
728+
<Button testId="valid" xcss={stylesValid.primary} />
729+
<Button
730+
testId="invalid"
731+
// @ts-expect-error — This is not assignable as it's missing the required `background` property.
732+
xcss={stylesInvalid.primary}
733+
/>
734+
</>
735+
);
736+
737+
expect(getByTestId('button-valid')).toHaveCompiledCss('color', 'var(--ds-text-hover)', {
738+
target: ':hover',
739+
});
740+
expect(getByTestId('button-invalid')).toHaveCompiledCss('color', 'var(--ds-text)', {
741+
target: ':focus',
742+
});
560743
});
561744
});
562745

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

+17-28
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,18 @@
1-
import type { StrictCSSProperties, CSSPseudos } from '../types';
1+
import type { StrictCSSProperties, CSSPseudos, CSSProps } from '../types';
22
import { createStrictSetupError } from '../utils/error';
33
import { type CompiledStyles, cx, type Internal$XCSSProp } from '../xcss-prop';
44

5-
type PseudosDeclarations = {
6-
[Q in CSSPseudos]?: StrictCSSProperties;
7-
};
5+
import type { AllowedStyles, ApplySchema, ApplySchemaMap, CompiledSchemaShape } from './types';
86

9-
type EnforceSchema<TSchema> = {
10-
[P in keyof TSchema]?: P extends keyof CompiledSchema
11-
? TSchema[P] extends Record<string, any>
12-
? EnforceSchema<TSchema[P]>
13-
: TSchema[P]
14-
: never;
15-
};
16-
17-
type CSSStyles<TSchema extends CompiledSchema> = StrictCSSProperties &
18-
PseudosDeclarations &
19-
EnforceSchema<TSchema>;
20-
21-
type CSSMapStyles<TSchema extends CompiledSchema> = Record<string, CSSStyles<TSchema>>;
22-
23-
interface CompiledAPI<TSchema extends CompiledSchema> {
7+
export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
248
/**
259
* ## CSS
2610
*
2711
* Creates styles that are statically typed and useable with other Compiled APIs.
2812
* For further details [read the documentation](https://compiledcssinjs.com/docs/api-css).
2913
*
14+
* This API does not currently work with XCSS prop.
15+
*
3016
* @example
3117
* ```
3218
* const redText = css({
@@ -36,7 +22,12 @@ interface CompiledAPI<TSchema extends CompiledSchema> {
3622
* <div css={redText} />
3723
* ```
3824
*/
39-
css(styles: CSSStyles<TSchema>): StrictCSSProperties;
25+
css<TStyles extends ApplySchema<TStyles, TSchema>>(
26+
styles: AllowedStyles & TStyles
27+
// NOTE: This return type is deliberately not using ReadOnly<CompiledStyles<TStyles>>
28+
// So it type errors when used with XCSS prop. When we update the compiler to work with
29+
// it we can update the return type so it stops being a type violation.
30+
): CSSProps<unknown>;
4031
/**
4132
* ## CSS Map
4233
*
@@ -53,11 +44,11 @@ interface CompiledAPI<TSchema extends CompiledSchema> {
5344
* <div css={styles.solid} />
5445
* ```
5546
*/
56-
cssMap<TStylesMap extends CSSMapStyles<TSchema>>(
57-
// We intersection type the generic both with the concrete type and the generic to ensure the output has the generic applied.
58-
// Without both it would either have the input arg not have excess property check kick in allowing unexpected values or
59-
// have all values set as the output making usage with XCSSProp have type violations unexpectedly.
60-
styles: CSSMapStyles<TSchema> & TStylesMap
47+
cssMap<
48+
TObject extends Record<string, AllowedStyles>,
49+
TStylesMap extends ApplySchemaMap<TObject, TSchema>
50+
>(
51+
styles: Record<string, AllowedStyles> & TStylesMap
6152
): {
6253
readonly [P in keyof TStylesMap]: CompiledStyles<TStylesMap[P]>;
6354
};
@@ -149,8 +140,6 @@ interface CompiledAPI<TSchema extends CompiledSchema> {
149140
>(): Internal$XCSSProp<TAllowedProperties, TAllowedPseudos, TSchema, TRequiredProperties>;
150141
}
151142

152-
type CompiledSchema = StrictCSSProperties & PseudosDeclarations;
153-
154143
/**
155144
* ## Create Strict API
156145
*
@@ -199,7 +188,7 @@ type CompiledSchema = StrictCSSProperties & PseudosDeclarations;
199188
* <div css={styles} />
200189
* ```
201190
*/
202-
export function createStrictAPI<TSchema extends CompiledSchema>(): CompiledAPI<TSchema> {
191+
export function createStrictAPI<TSchema extends CompiledSchemaShape>(): CompiledAPI<TSchema> {
203192
return {
204193
css() {
205194
throw createStrictSetupError();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type {
2+
StrictCSSProperties,
3+
CSSPseudoClasses,
4+
CSSPseudoElements,
5+
CSSPseudos,
6+
} from '../types';
7+
8+
/**
9+
* This is the shape of the generic object that `createStrictAPI()` takes.
10+
* It's deliberately a subset of `AllowedStyles` and does not take at rules
11+
* and pseudo elements.
12+
*/
13+
export type CompiledSchemaShape = StrictCSSProperties & {
14+
[Q in CSSPseudoClasses]?: StrictCSSProperties;
15+
};
16+
17+
export type PseudosDeclarations = { [Q in CSSPseudos]?: StrictCSSProperties };
18+
19+
export type AllowedStyles = StrictCSSProperties & PseudosDeclarations;
20+
21+
export type ApplySchemaValue<
22+
TSchema,
23+
TKey extends keyof StrictCSSProperties,
24+
TPseudoKey extends CSSPseudoClasses | ''
25+
> = TKey extends keyof TSchema
26+
? // TKey is a valid property on the schema
27+
TPseudoKey extends keyof TSchema
28+
? TKey extends keyof TSchema[TPseudoKey]
29+
? // We found a more specific value under TPseudoKey.
30+
TSchema[TPseudoKey][TKey]
31+
: // Did not found anything specific, use the top level TSchema value.
32+
TSchema[TKey]
33+
: // Did not found anything specific, use the top level TSchema value.
34+
TSchema[TKey]
35+
: // TKey wasn't found on the schema, fallback to the CSS property value
36+
StrictCSSProperties[TKey];
37+
38+
/**
39+
* Recursively maps over object properties to resolve them to either a {@link TSchema}
40+
* value if present, else fallback to its value from {@link StrictCSSProperties}. If
41+
* the property isn't a known property its value will be resolved to `never`.
42+
*/
43+
export type ApplySchema<TObject, TSchema, TPseudoKey extends CSSPseudoClasses | '' = ''> = {
44+
[TKey in keyof TObject]?: TKey extends keyof StrictCSSProperties
45+
? // TKey is a valid CSS property, try to resolve its value.
46+
ApplySchemaValue<TSchema, TKey, TPseudoKey>
47+
: TKey extends CSSPseudoClasses
48+
? // TKey is a valid pseudo class, recursively resolve its child properties
49+
// while passing down the parent pseudo key to resolve any specific schema types.
50+
ApplySchema<TObject[TKey], TSchema, TKey>
51+
: TKey extends `@${string}` | CSSPseudoElements
52+
? // TKey is either an at rule or a pseudo element, either way we don't care about
53+
// passing down the key so we recursively resolve its child properties starting at
54+
// the base schema, treating it as if it's not inside an object.
55+
ApplySchema<TObject[TKey], TSchema>
56+
: // Fallback case, did not find a valid CSS property, at rule, or pseudo.
57+
// Resolve the value to `never` which will end up being a type violation.
58+
never;
59+
};
60+
61+
export type ApplySchemaMap<TStylesMap, TSchema> = {
62+
[P in keyof TStylesMap]: ApplySchema<TStylesMap[P], TSchema>;
63+
};

‎packages/react/src/css/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { createSetupError } from '../utils/error';
99
* Create styles that are statically typed and useable with other Compiled APIs.
1010
* For further details [read the documentation](https://compiledcssinjs.com/docs/api-css).
1111
*
12+
* This API does not currently work with XCSS prop.
13+
*
1214
* ### Style with objects
1315
*
1416
* @example

‎packages/react/src/types.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,7 @@ export type CssFunction<TProps = unknown> =
3535
| boolean // Something like `false && styles`
3636
| undefined; // Something like `undefined && styles`
3737

38-
/*
39-
* This list of pseudo-classes and pseudo-elements are from csstype
40-
* but with & added to the front. Compiled supports both &-ful
41-
* and &-less forms and both will target the current element
42-
* (`&:hover` <==> `:hover`), however we force the use of the
43-
* &-ful form for consistency with the nested spec for new APIs.
44-
*/
45-
export type CSSPseudos =
38+
export type CSSPseudoElements =
4639
| '&::after'
4740
| '&::backdrop'
4841
| '&::before'
@@ -56,7 +49,9 @@ export type CSSPseudos =
5649
| '&::selection'
5750
| '&::spelling-error'
5851
| '&::target-text'
59-
| '&::view-transition'
52+
| '&::view-transition';
53+
54+
export type CSSPseudoClasses =
6055
| '&:active'
6156
| '&:autofill'
6257
| '&:blank'
@@ -94,6 +89,15 @@ export type CSSPseudos =
9489
| '&:valid'
9590
| '&:visited';
9691

92+
/*
93+
* This list of pseudo-classes and pseudo-elements are from csstype
94+
* but with & added to the front. Compiled supports both &-ful
95+
* and &-less forms and both will target the current element
96+
* (`&:hover` <==> `:hover`), however we force the use of the
97+
* &-ful form for consistency with the nested spec for new APIs.
98+
*/
99+
export type CSSPseudos = CSSPseudoElements | CSSPseudoClasses;
100+
97101
/**
98102
* The XCSSProp must be given all known available properties even
99103
* if it takes a subset of them. This ensures the (lack-of an)

‎packages/react/src/xcss-prop/index.ts

+17-14
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import type * as CSS from 'csstype';
22

3+
import type { ApplySchemaValue } from '../create-strict-api/types';
34
import { ac } from '../runtime';
4-
import type { CSSPseudos, CSSProperties, StrictCSSProperties } from '../types';
5+
import type { CSSPseudos, CSSPseudoClasses, CSSProperties, StrictCSSProperties } from '../types';
56

67
type MarkAsRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
78

8-
type XCSSItem<TStyleDecl extends keyof CSSProperties, TSchema> = {
9-
[Q in keyof CSSProperties]: Q extends TStyleDecl
10-
?
11-
| CompiledPropertyDeclarationReference
12-
| (Q extends keyof TSchema ? TSchema[Q] : CSSProperties[Q])
9+
type XCSSValue<
10+
TStyleDecl extends keyof CSSProperties,
11+
TSchema,
12+
TPseudoKey extends CSSPseudoClasses | ''
13+
> = {
14+
[Q in keyof StrictCSSProperties]: Q extends TStyleDecl
15+
? ApplySchemaValue<TSchema, Q, TPseudoKey>
1316
: never;
1417
};
1518

16-
type XCSSPseudos<
17-
TAllowedProperties extends keyof CSSProperties,
19+
type XCSSPseudo<
20+
TAllowedProperties extends keyof StrictCSSProperties,
1821
TAllowedPseudos extends CSSPseudos,
1922
TRequiredProperties extends { requiredProperties: TAllowedProperties },
2023
TSchema
2124
> = {
2225
[Q in CSSPseudos]?: Q extends TAllowedPseudos
2326
? MarkAsRequired<
24-
XCSSItem<TAllowedProperties, Q extends keyof TSchema ? TSchema[Q] : object>,
27+
XCSSValue<TAllowedProperties, TSchema, Q extends CSSPseudoClasses ? Q : ''>,
2528
TRequiredProperties['requiredProperties']
2629
>
2730
: never;
@@ -54,7 +57,7 @@ type CompiledPropertyDeclarationReference = {
5457
export type CompiledStyles<TObject> = {
5558
[Q in keyof TObject]: TObject[Q] extends Record<string, unknown>
5659
? CompiledStyles<TObject[Q]>
57-
: CompiledPropertyDeclarationReference;
60+
: CompiledPropertyDeclarationReference & TObject[Q];
5861
};
5962

6063
/**
@@ -134,7 +137,7 @@ export type XCSSAllPseudos = CSSPseudos;
134137
* To concatenate and conditonally apply styles use the {@link cssMap} {@link cx} functions.
135138
*/
136139
export type XCSSProp<
137-
TAllowedProperties extends keyof CSSProperties,
140+
TAllowedProperties extends keyof StrictCSSProperties,
138141
TAllowedPseudos extends CSSPseudos,
139142
TRequiredProperties extends {
140143
requiredProperties: TAllowedProperties;
@@ -143,7 +146,7 @@ export type XCSSProp<
143146
> = Internal$XCSSProp<TAllowedProperties, TAllowedPseudos, object, TRequiredProperties>;
144147

145148
export type Internal$XCSSProp<
146-
TAllowedProperties extends keyof CSSProperties,
149+
TAllowedProperties extends keyof StrictCSSProperties,
147150
TAllowedPseudos extends CSSPseudos,
148151
TSchema,
149152
TRequiredProperties extends {
@@ -152,11 +155,11 @@ export type Internal$XCSSProp<
152155
}
153156
> =
154157
| (MarkAsRequired<
155-
XCSSItem<TAllowedProperties, TSchema>,
158+
XCSSValue<TAllowedProperties, TSchema, ''>,
156159
TRequiredProperties['requiredProperties']
157160
> &
158161
MarkAsRequired<
159-
XCSSPseudos<TAllowedProperties, TAllowedPseudos, TRequiredProperties, TSchema>,
162+
XCSSPseudo<TAllowedProperties, TAllowedPseudos, TRequiredProperties, TSchema>,
160163
TRequiredProperties['requiredPseudos']
161164
> &
162165
BlockedRules)

0 commit comments

Comments
 (0)
Please sign in to comment.