Skip to content

Commit ba65efc

Browse files
Sprinkles: Add RequiredConditionalValue type (#157)
1 parent f5ab957 commit ba65efc

File tree

7 files changed

+160
-20
lines changed

7 files changed

+160
-20
lines changed

.changeset/selfish-maps-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vanilla-extract/sprinkles': minor
3+
---
4+
5+
Add `RequiredConditionalValue` type

packages/sprinkles/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ document.write(`
234234
- [createNormalizeValueFn](#createnormalizevaluefn)
235235
- [Types](#types)
236236
- [ConditionalValue](#conditionalvalue)
237+
- [RequiredConditionalValue](#requiredconditionalvalue)
237238
- [Thanks](#thanks)
238239
- [License](#license)
239240

@@ -619,6 +620,37 @@ const b: ResponsiveAlign = { mobile: 'center', desktop: 'left' };
619620
const c: ResponsiveAlign = ['center', null, 'left'];
620621
```
621622

623+
### RequiredConditionalValue
624+
625+
Same as [ConditionalValue](#conditionalvalue) except the default condition is required. For example, if your default condition was `'mobile'`, then a conditional value of `{ desktop: '...' }` would be a type error.
626+
627+
```ts
628+
import { createAtomicStyles, RequiredConditionalValue } from '@vanilla-extract/sprinkles';
629+
630+
const responsiveStyles = createAtomicStyles({
631+
defaultCondition: 'mobile',
632+
// etc.
633+
});
634+
635+
export type RequiredResponsiveValue<Value extends string | number> = RequiredConditionalValue<typeof responsiveStyles, Value>;
636+
```
637+
638+
You can then import the generated type in your app code.
639+
640+
```ts
641+
import { RequiredResponsiveValue } from './sprinkles.css.ts';
642+
643+
type ResponsiveAlign = RequiredResponsiveValue<'left' | 'center' | 'right'>;
644+
645+
const a: ResponsiveAlign = 'left';
646+
const b: ResponsiveAlign = { mobile: 'center', desktop: 'left' };
647+
const c: ResponsiveAlign = ['center', null, 'left'];
648+
649+
// Type errors:
650+
const d: ResponsiveAlign = [null, 'center'];
651+
const e: ResponsiveAlign = { desktop: 'center' };
652+
```
653+
622654
---
623655

624656
## Thanks

packages/sprinkles/src/createAtomsFn.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
ResponsiveArray,
2+
ResponsiveArrayByMaxLength,
33
ConditionalPropertyValue,
44
AtomicStyles,
55
ConditionalWithResponsiveArrayProperty,
@@ -11,7 +11,7 @@ import {
1111
type ResponsiveArrayVariant<
1212
RA extends { length: number },
1313
Values extends string | number | symbol
14-
> = ResponsiveArray<RA['length'], Values | null>;
14+
> = ResponsiveArrayByMaxLength<RA['length'], Values | null>;
1515

1616
type ConditionalStyle<
1717
Values extends { [key: string]: ConditionalPropertyValue }

packages/sprinkles/src/createUtils.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { addRecipe } from '@vanilla-extract/css/recipe';
2-
import { ResponsiveArray } from './types';
2+
import {
3+
ResponsiveArrayByMaxLength,
4+
RequiredResponsiveArrayByMaxLength,
5+
} from './types';
36

47
type ExtractValue<
58
Value extends
69
| string
710
| number
811
| Partial<Record<string, string | number>>
9-
| ResponsiveArray<number, string | number | null>
10-
> = Value extends ResponsiveArray<number, string | number | null>
12+
| ResponsiveArrayByMaxLength<number, string | number | null>
13+
> = Value extends ResponsiveArrayByMaxLength<number, string | number | null>
1114
? NonNullable<Value[number]>
1215
: Value extends Partial<Record<string, string | number>>
1316
? NonNullable<Value[keyof Value]>
@@ -36,12 +39,43 @@ export type ConditionalValue<
3639
| (ExtractDefaultCondition<AtomicStyles> extends false ? never : Value)
3740
| Partial<Record<ExtractConditionNames<AtomicStyles>, Value>>
3841
| (AtomicStyles['conditions']['responsiveArray'] extends { length: number }
39-
? ResponsiveArray<
42+
? ResponsiveArrayByMaxLength<
4043
AtomicStyles['conditions']['responsiveArray']['length'],
41-
Value | null
44+
Value
4245
>
4346
: never);
4447

48+
type RequiredConditionalObject<
49+
RequiredConditionName extends string,
50+
OptionalConditionNames extends string,
51+
Value extends string | number
52+
> = Record<RequiredConditionName, Value> &
53+
Partial<Record<OptionalConditionNames, Value>>;
54+
55+
export type RequiredConditionalValue<
56+
AtomicStyles extends Conditions<string>,
57+
Value extends string | number
58+
> = ExtractDefaultCondition<AtomicStyles> extends false
59+
? never
60+
:
61+
| Value
62+
| RequiredConditionalObject<
63+
Exclude<ExtractDefaultCondition<AtomicStyles>, false>,
64+
Exclude<
65+
ExtractConditionNames<AtomicStyles>,
66+
ExtractDefaultCondition<AtomicStyles>
67+
>,
68+
Value
69+
>
70+
| (AtomicStyles['conditions']['responsiveArray'] extends {
71+
length: number;
72+
}
73+
? RequiredResponsiveArrayByMaxLength<
74+
AtomicStyles['conditions']['responsiveArray']['length'],
75+
Value
76+
>
77+
: never);
78+
4579
export function createNormalizeValueFn<AtomicStyles extends Conditions<string>>(
4680
atomicStyles: AtomicStyles,
4781
): <Value extends string | number>(

packages/sprinkles/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { AtomicStyles, ResponsiveArrayConfig } from './types';
99

1010
export { createNormalizeValueFn, createMapValueFn } from './createUtils';
11-
export type { ConditionalValue } from './createUtils';
11+
export type { ConditionalValue, RequiredConditionalValue } from './createUtils';
1212

1313
interface Condition {
1414
'@media'?: string;

packages/sprinkles/src/types.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
1-
interface Tuple<Length extends number, Value> extends ReadonlyArray<Value> {
1+
interface ResponsiveArray<Length extends number, Value>
2+
extends ReadonlyArray<Value> {
23
0: Value;
34
length: Length;
45
}
56

6-
export type ResponsiveArrayConfig<Value> = Tuple<2 | 3 | 5 | 6 | 7 | 8, Value>;
7+
interface RequiredResponsiveArray<Length extends number, Value>
8+
extends ReadonlyArray<Value> {
9+
0: Exclude<Value, null>;
10+
length: Length;
11+
}
12+
13+
export type ResponsiveArrayConfig<Value> = ResponsiveArray<
14+
2 | 3 | 5 | 6 | 7 | 8,
15+
Value
16+
>;
17+
18+
export type ResponsiveArrayByMaxLength<MaxLength extends number, Value> = [
19+
never,
20+
ResponsiveArray<1, Value | null>,
21+
ResponsiveArray<1 | 2, Value | null>,
22+
ResponsiveArray<1 | 2 | 3, Value | null>,
23+
ResponsiveArray<1 | 2 | 3 | 4, Value | null>,
24+
ResponsiveArray<1 | 2 | 3 | 4 | 5, Value | null>,
25+
ResponsiveArray<1 | 2 | 3 | 4 | 5 | 6, Value | null>,
26+
ResponsiveArray<1 | 2 | 3 | 4 | 5 | 6 | 7, Value | null>,
27+
ResponsiveArray<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, Value | null>,
28+
][MaxLength];
729

8-
export type ResponsiveArray<Count extends number, Value> = [
30+
export type RequiredResponsiveArrayByMaxLength<
31+
MaxLength extends number,
32+
Value
33+
> = [
934
never,
10-
Tuple<1, Value>,
11-
Tuple<1 | 2, Value>,
12-
Tuple<1 | 2 | 3, Value>,
13-
Tuple<1 | 2 | 3 | 4, Value>,
14-
Tuple<1 | 2 | 3 | 4 | 5, Value>,
15-
Tuple<1 | 2 | 3 | 4 | 5 | 6, Value>,
16-
Tuple<1 | 2 | 3 | 4 | 5 | 6 | 7, Value>,
17-
Tuple<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, Value>,
18-
][Count];
35+
RequiredResponsiveArray<1, Value | null>,
36+
RequiredResponsiveArray<1 | 2, Value | null>,
37+
RequiredResponsiveArray<1 | 2 | 3, Value | null>,
38+
RequiredResponsiveArray<1 | 2 | 3 | 4, Value | null>,
39+
RequiredResponsiveArray<1 | 2 | 3 | 4 | 5, Value | null>,
40+
RequiredResponsiveArray<1 | 2 | 3 | 4 | 5 | 6, Value | null>,
41+
RequiredResponsiveArray<1 | 2 | 3 | 4 | 5 | 6 | 7, Value | null>,
42+
RequiredResponsiveArray<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, Value | null>,
43+
][MaxLength];
1944

2045
export type ConditionalPropertyValue = {
2146
defaultClass: string | undefined;

tests/sprinkles/spinkles-type-tests.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createMapValueFn,
77
createNormalizeValueFn,
88
ConditionalValue,
9+
RequiredConditionalValue,
910
} from '@vanilla-extract/sprinkles';
1011
import { createAtomsFn } from '@vanilla-extract/sprinkles/createAtomsFn';
1112

@@ -210,4 +211,47 @@ const noop = (...args: Array<any>) => {};
210211
responsiveValue = { NOPE: 123 };
211212

212213
noop(responsiveValue);
214+
215+
type RequiredResponsiveValue<
216+
Value extends string | number
217+
> = RequiredConditionalValue<typeof conditionalAtomicStyles, Value>;
218+
219+
let requiredValue: RequiredResponsiveValue<'row' | 'column'>;
220+
221+
// Valid values
222+
requiredValue = 'row';
223+
requiredValue = { mobile: 'row' };
224+
requiredValue = { mobile: 'row', desktop: 'column' };
225+
requiredValue = ['row'];
226+
requiredValue = ['row', null, 'column'];
227+
228+
// @ts-expect-error
229+
requiredValue = [];
230+
// @ts-expect-error
231+
requiredValue = [null];
232+
// @ts-expect-error
233+
requiredValue = [null, 'column'];
234+
// @ts-expect-error
235+
requiredValue = [null, null, 'column'];
236+
// @ts-expect-error
237+
requiredValue = {};
238+
// @ts-expect-error
239+
requiredValue = { desktop: 'column' };
240+
241+
noop(requiredValue);
242+
243+
// Ensure type is 'never' when default condition is missing
244+
type InvalidRequiredResponsiveValue<
245+
Value extends string | number
246+
> = RequiredConditionalValue<
247+
typeof conditionalStylesWithoutDefaultCondition,
248+
Value
249+
>;
250+
251+
const invalidRequiredValue: InvalidRequiredResponsiveValue<
252+
'row' | 'column'
253+
// @ts-expect-error
254+
> = ['row'];
255+
256+
noop(invalidRequiredValue);
213257
};

0 commit comments

Comments
 (0)