Readiness: Implemented
The toolchain
Type checking
uf check is uf lint followed by Flow's type inference — Meta's own Flow,
ported to Rust and compiled into the binary. uf has no second opinion about your
types. What it decides is which files Flow sees, which packages it can read, and
how the answer is reported.
What you will be able to do: check a project or a few files in it, read the
human report and the JSON one, tell why an import is typed any, and know what
makes a check fail.
What you need first: a project with its dependencies installed —
Dependencies. A check run before uf install types every
package it cannot find as any, which is the reason
uf in CI installs before it checks.
Running it
Two files, one of them wrong:
// @flow
// lib/total.js
export type LineItem = {| readonly name: string, readonly cents: number |};
export function total(items: $ReadOnlyArray<LineItem>): number {
return items.reduce((sum, item) => sum + item.cents, 0);
}
// @flow
// lib/summary.js
import { total } from "./total.js";
export const due: string = total([{ name: "tea", cents: 450 }]);
uf check · my-app
─────────────────
lib/summary.js 1 error
error[incompatible-type]: Cannot assign total(...) to due because number [1] is incompatible with string [2].
--> lib/summary.js:4:28
│
4 │ export const due: string = total([{ name: "tea", cents: 450 }]);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: [1] is here
--> lib/total.js:4:57
note: [2] is here
--> lib/summary.js:4:19
types checked 46
asked about 10 of 46
unchanged 44 of 46
inference 29.7ms
builtins 228.6ms (cold)
› these imports are typed as any; uf resolved no module for them
- @uniflowed/host/module-mocks
- axe-core
- node:async_hooks
- node:module
- react-dom
✗ 1 error · 10 files checked · 412.3ms
error: uf check failed with 1 error
The first half of the report is uf lint's, and a lint error fails uf check as
surely as a type error does; on the same two files uf lint alone reports no
problems. The second half is inference: types checked counts every file Flow
read, asked about the ones the run was for, and unchanged the ones answered
from the cache. The verdict comes last, on one line: the total across both
halves, the files the linter read, and how long the whole run took. The run
exits 1, with the sentence on the last line written to stderr.
--fix and --fix-unsafe apply uf lint's fixes before the check, the way they
do for uf lint. Nothing fixes a type error.
What it checks
Every .js, .jsx, .mjs and .cjs file in the project, and each
package.json, found the way uf lint finds them: .gitignore is honoured, the
top-level ignore list in uf.config.js is applied — node_modules, dist and
target by default — and .uf and .git are never read.
A file needs no // @flow to be checked. One that says // @noflow is parsed,
so a syntax error in it is still reported, and not inferred; the footer counts it
on a row of its own:
types checked 46
@noflow 1
A .ts or .tsx file is not checked at all. An import of one is typed any.
Checking some of the files
Name paths after the command, and only files whose path contains one of them are reported:
$ uf check lib/summary.js
types checked 2
asked about 1 of 2
✗ 1 error · 1 file checked · 301.8ms
A narrowed check still reads what the named file imports — total.js above is the
second of the two files typed — and reports only the named file's own errors. The
path is a substring of a path relative to the project root, not a glob and not
relative to your shell: uf check Counter.js also checks useCounter.js, and a
leading ./ matches nothing:
$ uf check ./lib/summary.js
error: no file matched `./lib/summary.js`
What an import resolves to
| An import of | Resolves to |
|---|---|
a file in the project, with or without its extension, or a directory with an index file | that file, typed |
a module a library definition declares — react, node:fs and the rest Flow ships | the declaration, whatever node_modules holds |
a package installed in node_modules that ships Flow — a file with @flow in its first 8 KiB, or a .flow file beside a module | that package's files, through its exports map, each .flow file read in place of the module it sits beside |
a package installed in node_modules that ships TypeScript declarations and no Flow — its own, or an @types package's | a Flow translation of the declarations TypeScript reads for it, with what the translation cannot say typed any and counted in the report |
| a package in the same workspace | the file its manifest publishes |
an image, a stylesheet or ?raw, and import.meta.glob or import.meta.hot | uf's declarations for Vite's client API |
| anything else | any, and named in the report |
An exports map is read with the flow, import and default conditions and no
others: uf does not choose between node, browser, bun and deno, so a
package that publishes only host-specific entries is typed any, and the JSON
report lists it apart from a package that is missing. A package with no exports
map is read through module, then main. A .json import is typed any.
A package's TypeScript declarations are found the way TypeScript finds them: the
types condition beside import and default, the types and typings fields
and typesVersions when there is no exports map, and @types/<name> —
@types/scope__name for a scoped package — when the package ships none. The
report names each package typed that way once, with how many of its places are
any and how many errors Flow reports inside the translation, and
typeCheck.translatedPackages in --json carries the same. A second check reads
the translation back from .uf/cache/dts rather than making it again.
uf check --explain-any <package> lists each hole and each of those errors with
the declaration file, the line, and the declaration it is in.
The names in these imports are typed as any are where the check cannot see.
Before uf install, that list is every @uniflowed/* package the scaffold
imports, and the scaffold still checks clean, because it only uses them as values.
Using a type from a module that resolved to nothing is an error, not a quiet
any:
lib/price.js 1 error
error[value-as-type]: Cannot use Money as a type because it is an any-typed value. Type Money properly, so it is no longer any-typed, to use it as an annotation.
--> lib/price.js:4:21
│
4 │ export const price: Money = 450;
│ ^^^^^
› these imports are typed as any; uf resolved no module for them
- money-that-is-not-installed
Library definitions and .flowconfig
Flow's own library definitions come first — the DOM, Node's built-ins, React —
then uf's for Vite's client API and Web Crypto, then the project's: every file
under flow-typed/, which needs no configuration, and whatever the [libs]
section of a .flowconfig names, in order. A later definition shadows an earlier
one. uf explain check gives the same order, with where a dependency's types
come from between the two:
uf check
1. library definitions
provider flow (upstream)
what Flow's own, then `flow-typed` and whatever `.flowconfig`'s [libs] names
2. dependencies
provider uf
what a package's own Flow when it ships some; otherwise a Flow translation of its TypeScript declarations, each hole typed any — `uf check --explain-any <package>` lists them
3. type checking
provider flow (upstream)
what uf does not type-check; Flow is the type system
[libs] is the only section of .flowconfig uf reads. [ignore], [options],
[lints] and [version] have no effect — the top-level ignore in
uf.config.js is where files are left out — but a .flowconfig that does not
parse fails the check rather than being skipped.
The report a machine reads
uf check --json writes the whole report to stdout and nothing else:
{
"command": "uf check",
"filesChecked": 10,
"errors": 1,
"warnings": 0,
"diagnostics": [],
"unavailableRules": [],
"typeCheck": {
"backend": "upstream-flow-rust-port",
"status": "checked",
"diagnostics": [
{
"severity": "error",
"kind": "infer",
"code": "incompatible-type",
…
It is uf lint --json's report with typeCheck added, and one field is a trap:
errors counts both halves, and diagnostics holds only the lint findings.
The type errors are in typeCheck.diagnostics, so a project can fail with
diagnostics empty, as this one does. unavailableRules is empty once
inference has run, because those rules ran during the check; uf lint is what
still names them. typeCheck.status is checked, unavailable or failed,
with the reason in typeCheck.error when it failed.
The command reference lists every field.
Speed
Inference is cached under .uf/cache/check/, per file, keyed on the file, the
project's library definitions and the uf binary that produced the answer — so a
new uf starts cold, and an edit re-checks the file and what imports it. The
builtin library definitions are merged once per process and are not cached, which
is the builtins row: about 200 ms on every run, however warm the rest is. The
JSON report carries the same numbers in typeCheck: filesChecked, requested,
filesFromCache, elapsedMs, builtinsMs and builtinsCold, with the imports
typed any in untypedModules.
The lint half runs on every core. Inference runs on one thread. What the React
Compiler reports about each module — nearly all of what the lint half costs — is
cached under .uf/cache/lint/, keyed on the module's path and text, the React
rules' switches and the uf binary, so uf lint and uf check hand only the
modules that changed to the compiler again. There is no watch
mode and no daemon; uf check starts, answers and exits. The editor is where the
same checker stays warm: uf lsp pushes these errors for the files you have open
once typing pauses, from the same modules and suppression comments, beside hover,
go to definition and member completion — see IDE setup.
Silencing one error
$FlowFixMe and $FlowExpectedError suppress the error on the line below them,
best written with the code they expect — // $FlowFixMe[incompatible-type] — so
that a different error on the same line is still reported. A suppression that
suppresses nothing is reported as a warning, "Unused suppression comment.", on
the comment itself, as Flow reports it. It is a warning rather than an error, so
a stale comment does not fail a run. Delete it, or correct its code: an error
code Flow has since renamed is the usual reason a working suppression stops
matching.
What is not here
- The lint rules that need inference. The last line's hint counts them,
uf lint --rulesmarks them, and the JSON names them. They do not run underuf lint, and they do not run underuf checkeither, although inference does. - A type check in
uf prepare, the pre-commit command. - More than one exit code for failure. A lint error, a type error and a
.flowconfigthat does not parse all exit 1; only an argument uf cannot parse exits 2. - A type defined as itself across two files. A pair of aliases that refer to
each other through imports is typed
anyrather than refused.
Where to go next
Formatting and linting is next: uf fmt and uf lint, over
the same syntax tree. uf in CI runs uf check after uf install,
and Flow and TypeScript is the argument for the type system
this command runs.
Edit this pagedocs/app/guide/check/$page.mdx