Skip to content

[types] Hand-maintain StyleXCSSTypes.d.ts for autocompletable unions - #1882

Draft
henryqdineen wants to merge 1 commit into
mainfrom
hqd-stylex-css-types-dts
Draft

henryqdineen wants to merge 1 commit into
mainfrom
hqd-stylex-css-types-dts

Conversation

@henryqdineen

@henryqdineen henryqdineen commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

What changed / motivation ?

First of five stacked PRs on CSSProperties. The goal across them is to make the CSS types more useful, more complete, and more correct. Useful, because a union that accepts any string currently offers no autocomplete at all. Complete, because 522 properties and ~2,500 keywords written by hand have drifted from CSS. Correct, because some of what is there is wrong — every property was missing revert, and six accept no CSS-wide keywords at all, so colorScheme: 'inherit' is a type error today.

They are ordered so each one enables the next: the autocompletable definitions come first, then the tests that check them against CSS, then the fixes those tests found.

1 this PR Check in a hand-maintained StyleXCSSTypes.d.ts, so unions keep their literals
2 #1883 Test the types against the CSS value grammar and Baseline, with the known gaps allowlisted and explained
3 #1884 Accept revert and revert-layer; fix the six properties that accept no CSS-wide keywords
4 #1885 cursor should accept url() values (#1463)
5 #1886 Five worked examples of shrinking the allowlists — optional, 1–4 stand alone

Only the first two are infrastructure; 3 and 4 are bugs the tests found, and both are pure widenings.

This answers the question I asked in #1466: yes, maintaining a separate TypeScript definition of the CSS types is worth it, because the thing it enables cannot be expressed in Flow at all.

gen-types translates the Flow types into TypeScript, and that translation throws away the most useful thing these types can do, because a Flow union containing string collapses to string and discards every literal member:

Flow:       type Display = 'block' | 'flex' | string;
TypeScript: type Display = 'block' | 'flex' | (string & {});

string & {} is mutually assignable with string, so any string is still accepted, but the literals survive and editors keep suggesting them. (The trick explained.)

gen-types copies a .d.ts verbatim when one sits beside the Flow source, so checking this file in is enough to take over that translation. No build changes are needed.

100 of the 522 properties were typed as a bare string or number | string, so they offered no completions at all; roughly 340 more lost their literals to a string member. Those now autocomplete.

Linked PR/Issues

Answers the question raised in #1466.

Additional Context

The diff is deliberately minimal. This file is byte-identical to what gen-types already produced, once the 249 string(string & {}) substitutions are reversed. There are no type changes to audit: no properties added or removed, no keywords added or removed, nothing widened, nothing narrowed. Every string in the file is a plain union member or type-alias body — no index signatures, no Record<string, …>, no template literals, no generic arguments — so the substitution is mechanical.

Not a breaking change, and not a behavioural one. The unions that now end in (string & {}) previously ended in a bare string, which already accepted any string. Nothing new is accepted and nothing that typechecked before stops; the only change is that the literals are no longer erased.

packages/typescript-tests/src/open-unions.ts covers the behaviour: arbitrary strings and var() are still accepted, null still unsets a property, the CSS-wide keywords still resolve, literals survive, and closed unions stay closed.

One trap if you extend those tests: do not use NonNullable to inspect these unions. It is T & {} in TypeScript >= 4.9, and that intersection flattens them back down to string — so a test written with it will pass whether or not the literals actually survived, which is exactly the thing being tested. My first version of these tests had that bug.

The two files must now be kept in sync by hand. The next PR in this stack adds tests that enforce it — and writing those turned up a fair amount in the existing types that is incomplete, inconsistent, or simply wrong.

Pre-flight checklist

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

workflow: benchmarks/size

Comparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better.
yarn workspace v1.22.22
yarn run v1.22.22
$ node ./compare.js /tmp/tmp.2G53LupiQd /tmp/tmp.JzJflZikTu

Results Base Patch Ratio
@stylexjs/stylex/lib/cjs/stylex.js
· compressed 1,535 1,535 1.00
· minified 5,166 5,166 1.00
@stylexjs/stylex/lib/cjs/inject.js
· compressed 1,793 1,793 1.00
· minified 4,915 4,915 1.00
benchmarks/size/.build/bundle.js
· compressed 496,650 496,650 1.00
· minified 4,847,840 4,847,840 1.00
benchmarks/size/.build/stylex.css
· compressed 99,735 99,735 1.00
· minified 740,755 740,755 1.00
Done in 0.06s.
Done in 0.25s.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

workflow: benchmarks/perf

Comparison of performance test results, measured in operations per second. Larger is better.
yarn workspace v1.22.22
yarn run v1.22.22
$ node ./compare.js /tmp/tmp.8P13bLnejc /tmp/tmp.QZusocLeqD

Results Base Patch Ratio
babel-plugin: stylex.create
· basic create 595 582 0.98 -
· complex create 67 67 1.00
babel-plugin: stylex.createTheme
· basic themes 468 463 0.99 -
· complex themes 33 32 0.97 -
Done in 0.08s.
Done in 0.32s.

@henryqdineen
henryqdineen added this pull request to stack #1887 September 11, 2026 03:13
`gen-types` translates the Flow types into TypeScript, which loses the most
useful thing these types can do, because a Flow union containing `string`
collapses to `string` and discards every literal member:

    Flow:       type Display = 'block' | 'flex' | string;
    TypeScript: type Display = 'block' | 'flex' | (string & {});

`string & {}` is mutually assignable with `string`, so any string is still
accepted, but the literals survive and editors keep suggesting them. There is
no way to express this in Flow.

`gen-types` copies a `.d.ts` verbatim when one sits beside the Flow source, so
checking this file in is enough to take over that translation -- no build
changes. Verified byte-identical to what `gen-types` produced, once the 249
`string` -> `(string & {})` substitutions are reversed.

100 of the 522 properties were typed as bare `string` or `number | string` and
so offered no completions at all; another ~340 lost their literals to a
`string` member. Those now autocomplete.

`packages/typescript-tests/src/open-unions.ts` covers the behaviour: arbitrary
strings and `var()` are still accepted, `null` still unsets a property, the
CSS-wide keywords still resolve, literals survive, and closed unions stay
closed. Note `NonNullable` cannot be used to inspect these unions -- it is
`T & {}` in TypeScript >=4.9, and that intersection flattens them.

The two files must now be kept in sync by hand. Adding a css-tree-based test to
enforce that, and to check the literals against the CSS grammar, is the
intended follow-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@henryqdineen
henryqdineen force-pushed the hqd-stylex-css-types-dts branch from 98ae19c to 6d988f8 Compare September 11, 2026 03:15
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
stylex Ignored Ignored Sep 11, 2026 3:15am UTC

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant