Skip to content

fix(conform-react): preserve string literal types in DefaultValue - #1281

Closed
transparent-citizen wants to merge 5 commits into
edmundhung:mainfrom
transparent-citizen:main
Closed

fix(conform-react): preserve string literal types in DefaultValue#1281
transparent-citizen wants to merge 5 commits into
edmundhung:mainfrom
transparent-citizen:main

Conversation

@transparent-citizen

@transparent-citizen transparent-citizen commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

This adds a case to DefaultValue so that string literal types are preserved instead of being widened to string, making fields like submit intents type-safe.

const schema = z.object({
	status: z.literal('active'),
	role: z.enum(['admin', 'user']),
	name: z.string(),
});

const form = useForm({
	schema,
	defaultValue: {
		// ❌ before: string | null | undefined
		// ✅  after: 'active' | null | undefined
		status: 'active',

		// ❌ before: string | null | undefined
		// ✅  after: 'admin' | 'user' | null | undefined
		role: 'admin',

		// ✅ string | null | undefined (unchanged)
		name: 'John',
	},
});

The PR doesn't touch v1 in anticipation of its soon removal. If this is too soon, please don't hesitate to let me know, and I will retrofit it.

Generated Summary

DefaultValue now preserves string literal types in conform-react/future, including optional, nullable, template-literal, and nested values. Other types retain existing behavior. Type tests cover these cases and reject incompatible literals. Examples cast URL-derived defaults to schema-specific types. A patch changeset was added.

@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 822dbdf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@conform-to/react Minor
@conform-to/dom Minor
@conform-to/valibot Minor
@conform-to/yup Minor
@conform-to/zod Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

DefaultValue<Shape> now preserves string literal types instead of widening them to string. Compile-time tests cover literal strings, templates, object properties, arrays, nullish values, and invalid literals. Example forms cast URL-derived defaults to schema input types. A patch changeset documents the fix.

Changes

DefaultValue literal preservation

Layer / File(s) Summary
Preserve string literals and validate inference
packages/conform-react/future/types.ts, packages/conform-react/tests/types.test-d.ts, .changeset/olive-trees-narrow.md
DefaultValue<Shape> preserves string subtypes and nullish values. Compile-time tests cover literal inference and incompatible values. A patch changeset records the update.
Apply schema input types to examples
examples/headless-ui/src/App.tsx, examples/radix-ui/src/App.tsx, examples/react-aria/src/App.tsx, examples/shadcn-ui/src/App.tsx
The examples define schema input shapes and cast URL-derived defaults to the corresponding property types.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • edmundhung/conform#1256: Updates the Headless UI example’s schema and useForm default values, which this change types more explicitly.

Suggested reviewers: edmundhung

Poem

A rabbit checked the types at dawn,
Each literal value carried on.
URL defaults matched their shape,
Tests rejected values out of shape.
A patch note marked the change.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change to preserve string literal types in DefaultValue.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown
More templates

@conform-to/dom

npm i https://pkg.pr.new/@conform-to/dom@1281

@conform-to/react

npm i https://pkg.pr.new/@conform-to/react@1281

@conform-to/valibot

npm i https://pkg.pr.new/@conform-to/valibot@1281

@conform-to/validitystate

npm i https://pkg.pr.new/@conform-to/validitystate@1281

@conform-to/yup

npm i https://pkg.pr.new/@conform-to/yup@1281

@conform-to/zod

npm i https://pkg.pr.new/@conform-to/zod@1281

commit: 822dbdf

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/conform-react/future/types.ts`:
- Around line 198-200: Update the conditional type branch in DefaultValue<Shape>
so nullable or undefined constituents are handled separately while the
non-nullish string subset remains literal-preserving. Ensure a Shape such as {
intent?: 'save' | 'publish' } does not widen to string and therefore rejects
unsupported values like 'delete'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f601dce6-da2a-4d56-a3f5-aa6bf067d5ad

📥 Commits

Reviewing files that changed from the base of the PR and between 3f21351 and ca23dd0.

📒 Files selected for processing (3)
  • .changeset/olive-trees-narrow.md
  • packages/conform-react/future/types.ts
  • packages/conform-react/tests/types.test-d.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (14)
  • GitHub Check: Release
  • GitHub Check: E2E Tests (macos-latest, chromium, 22)
  • GitHub Check: E2E Tests (ubuntu-latest, chromium, 22)
  • GitHub Check: Future API Tests (ubuntu-latest, webkit, 22)
  • GitHub Check: Future API Tests (ubuntu-latest, chromium, 22)
  • GitHub Check: API Tests (v1)
  • GitHub Check: E2E Tests (ubuntu-latest, firefox, 22)
  • GitHub Check: Future API Tests (windows-latest, chromium, 22)
  • GitHub Check: Future API Tests (ubuntu-latest, firefox, 22)
  • GitHub Check: Future API Tests (macos-latest, chromium, 22)
  • GitHub Check: E2E Tests (macos-latest, webkit, 22)
  • GitHub Check: E2E Tests (ubuntu-latest, webkit, 22)
  • GitHub Check: E2E Tests (windows-latest, chromium, 22)
  • GitHub Check: Future API Tests (macos-latest, webkit, 22)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-04-05T20:22:11.135Z
Learnt from: edmundhung
Repo: edmundhung/conform PR: 1180
File: docs/api/react/future/useControl.md:361-363
Timestamp: 2026-04-05T20:22:11.135Z
Learning: When reviewing usages of `useControl.register` in `packages/conform-react`, consider both supported registration forms valid: (1) passing a single form control element (`HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFieldSetElement`), and (2) passing an iterable collection (`NodeList`/`HTMLCollection`) of checkbox/radio inputs that share the same `name` and compatible `type`. Do not flag the collection-based group registration as an error—both patterns are valid and maintained.

Applied to files:

  • packages/conform-react/tests/types.test-d.ts
  • packages/conform-react/future/types.ts
📚 Learning: 2026-07-18T14:14:07.849Z
Learnt from: edmundhung
Repo: edmundhung/conform PR: 1244
File: packages/conform-react/future/intent.ts:70-75
Timestamp: 2026-07-18T14:14:07.849Z
Learning: For the React “future” custom-state API, the `customState` handler map must be captured on form mount and remain unchanged when the form is reset. When reviewing changes to the future `useForm`/custom-state implementation, ensure reset logic does not recreate or mutate the captured handler map (the handler map should stay referentially stable per the contract described in `docs/api/react/future/useForm.md`).

Applied to files:

  • packages/conform-react/future/types.ts
🪛 markdownlint-cli2 (0.23.1)
.changeset/olive-trees-narrow.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🔇 Additional comments (2)
packages/conform-react/tests/types.test-d.ts (1)

123-157: LGTM!

.changeset/olive-trees-narrow.md (1)

1-5: LGTM!

Comment thread packages/conform-react/future/types.ts Outdated
@transparent-citizen

transparent-citizen commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

The examples are making use of unvalidated search params in defaultValue. Since unvalidated search params can contain any string and not just allowed literals, tests were failing.

In the real world, we would typically validate all search params before use to prevent unsound application state.

In the examples, I think that a type assertion is good enough to get the point across. By doing that, we're making the promise to TypeScript that these search params will have the correct type, even though TypeScript can show that there are ways for the type to be wider at runtime.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/radix-ui/src/App.tsx`:
- Around line 37-45: Normalize query-string defaults before constructing
defaultValue: in examples/radix-ui/src/App.tsx lines 37-45, validate carType,
userCountry, and desiredContractType against their schema literals and reject
unsupported values; in examples/headless-ui/src/App.tsx lines 61-62, validate
priority; in examples/react-aria/src/App.tsx lines 48-49, validate language and
every colors element; and in examples/shadcn-ui/src/App.tsx lines 76-86,
validate gender, job, accountType, categories, and nested object members.
Replace unsafe casts with the existing schema or equivalent type guards so
invalid URL values become the appropriate absent/default state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f0318bef-dc10-40fc-82c2-dc1f01722e65

📥 Commits

Reviewing files that changed from the base of the PR and between 083b78e and 251d9e6.

📒 Files selected for processing (4)
  • examples/headless-ui/src/App.tsx
  • examples/radix-ui/src/App.tsx
  • examples/react-aria/src/App.tsx
  • examples/shadcn-ui/src/App.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
  • GitHub Check: Release
  • GitHub Check: Future API Tests (ubuntu-latest, firefox, 22)
  • GitHub Check: E2E Tests (ubuntu-latest, firefox, 22)
  • GitHub Check: Future API Tests (ubuntu-latest, webkit, 22)
  • GitHub Check: Future API Tests (windows-latest, chromium, 22)
  • GitHub Check: E2E Tests (ubuntu-latest, webkit, 22)
  • GitHub Check: E2E Tests (ubuntu-latest, chromium, 22)
  • GitHub Check: E2E Tests (windows-latest, chromium, 22)
  • GitHub Check: E2E Tests (macos-latest, webkit, 22)
  • GitHub Check: E2E Tests (macos-latest, chromium, 22)
  • GitHub Check: API Tests (v1)
🔇 Additional comments (4)
examples/headless-ui/src/App.tsx (1)

36-37: LGTM!

examples/radix-ui/src/App.tsx (1)

25-26: LGTM!

examples/react-aria/src/App.tsx (1)

35-36: LGTM!

examples/shadcn-ui/src/App.tsx (1)

45-46: LGTM!

Comment thread examples/radix-ui/src/App.tsx Outdated
@transparent-citizen transparent-citizen changed the title fix(conform-react): preserve string literal types in DefaultValue [WIP] fix(conform-react): preserve string literal types in DefaultValue Jul 31, 2026
@transparent-citizen

transparent-citizen commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Coderabbit got mad at me for proposing type assertions 😅 So I went and validated all search params.

This should make the examples a little more streamlined, getting rid of the lengthy defaultValue objects that read from unvalidated data.

We now have type-safe default values in the examples, so working with string literals is a breeze. This also prevents invalid application state in the examples which was previously possible by constructing URLs that don't validate.

The only downside is that this changes the param semantics in the examples from "use all, might lead to invalid application state" to "use valid only, otherwise start with empty form". I'm not a zod expert, but maybe .partial could be useful to improve this in the future.

@transparent-citizen transparent-citizen changed the title [WIP] fix(conform-react): preserve string literal types in DefaultValue fix(conform-react): preserve string literal types in DefaultValue Aug 1, 2026
@transparent-citizen

Copy link
Copy Markdown
Contributor Author

The changeset was updated from patch to minor to reflect that users working with loosely-typed data will need to assert types or validate their data, similar to how the PR addresses the examples.

@edmundhung

Copy link
Copy Markdown
Owner

In the real world, we would typically validate all search params before use to prevent unsound application state.

I am not sure if that's something Conform can guarantee in the form levels. The default value could also be inferred from lastResult where the submission.payload could be whatever that being sent to the server. It's also up to the user to adjust the input value from the DOM. I don't see a good way to make sure defaultValue is valid all the time.

In fact, I have been meaning to make defaultValue accepts a raw URLSearchParams to simplify the example you touched here. Any concerns with #1284?

@transparent-citizen

transparent-citizen commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Hm, I see. The widening seems to have come up as a pain point in v1 before (#541), but I understand that defaultValue can't make such narrow type guarantees. This could be improved if Conform could validate the schemas internally, which would couple it to every supported validator. 😅

#1284 allows simplifying the examples similarly, without overpromising type guarantees that it can't keep. But it also allows for unsound application state, since malformed URLs can transfer arbitrary data into the application. This is not a regression, it was possible before. Prior validation outside of useForm, like this PR does, can prevent that, so in real-world applications, that's safer than passing URLSearchParams to useForm.

I think #1284 is good to merge nonetheless.

It feels like we're missing a validator-specific layer on top of useForm (say useZodForm) that helps make stronger guarantees.

@transparent-citizen

Copy link
Copy Markdown
Contributor Author

Closing. This needs a validation layer somewhere.

@edmundhung

Copy link
Copy Markdown
Owner

@transparent-citizen I think the narrowing you did is still useful. We can't narrow the output type (e.g. field.email.defaultValue) but restricting the defaultValue option can still be useful. The type could also be used to teach useControl() to only accept the expected enum with control.change().

@transparent-citizen

Copy link
Copy Markdown
Contributor Author

@edmundhung Just making sure I follow, is the suggestion to give the defaultValue option its own narrowed type, as opposed to changing DefaultValue?

@edmundhung

Copy link
Copy Markdown
Owner

@transparent-citizen Changing the DefaultValue type as you did is fine. It wouldn't change how we infer the metadata type (e.g. fields.something.defaultValue would remain be string).

The case in #541 were an issue only because it impact how we infer fields.something.value.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants