Repository navigation
fix: make the Exact helper compile on TypeScript 7.1 - #1293
Merged
Merged
Conversation
TypeScript 7.1 distinguishes the distributed `P` inside `P extends Builtin ? ... : ...` from the non-distributed `P` that the constraint `I extends P` refers to, so `I[K]` in the generated `Exact` helper fails with TS2344/TS2536 in every file that uses fromPartial/create. Drop the alias constraint (fromPartial/create already constrain `I extends Exact<DeepPartial<T>, I>`) and only map the keys of `P` that `I` actually has (`as Extract<K, keyof I>`), so that `I[K & keyof I]` type-checks. The resulting checks are unchanged: for keys missing from `I` the old `I[K]` resolved to `unknown`, i.e. no excess-property check for that key, and `P &` still contributes every property. Adds type tests for unknown properties at the top level, in nested and repeated messages and inside oneof cases, and re-generates the integration output.
infodusha
force-pushed
the
fix/exact-typescript-7-1
branch
from
October 20, 2026 20:55
d4631b4 to
2b221c8
Compare
infodusha
marked this pull request as draft
October 20, 2026 20:57
infodusha
marked this pull request as ready for review
October 20, 2026 21:00
Owner
|
This is great @infodusha , thank you! |
stephenh
pushed a commit
that referenced
this pull request
Oct 22, 2026
## [2.18.1](v2.18.0...v2.18.1) (2026-10-22) ### Bug Fixes * make the Exact helper compile on TypeScript 7.1 ([#1293](#1293)) ([1c5aca0](1c5aca0)), closes [microsoft/TypeScript#64237](microsoft/TypeScript#64237) [#64307](https://github.com/stephenh/ts-proto/issues/64307)
stephenh
pushed a commit
that referenced
this pull request
Nov 4, 2026
Fixes #1298 With `useNullAsOptional=true`, generated code stopped compiling in 2.18.1 when a nested message has an optional field. The nested `fromPartial` call fails with TS2345: `Type 'null' is not assignable to type 'string | undefined'`. `Exact<null, ...>` evaluates to `never`, because it reaches `null & {}`. So `Exact` drops `null` from optional fields. This is older than #1293: with the previous alias the generated code compiled, but `Data.fromPartial({ view: null })` was already rejected. After #1293 the generated nested call fails too. I checked both aliases on TypeScript 5.9.3, but I did not work out why the previous alias let the nested call through. This adds `null` to the generated `Builtin` type when `useNullAsOptional` is set, which is the workaround the issue suggests. `Exact` now stops at `null`. Besides fixing the compile error, this means `Data.fromPartial({ view: null })` now compiles, which matches the field type `string | null | undefined`. The generated files in `integration/use-null-as-optional` and `integration/no-defaults-for-optionals-with-nulls` change by that one line. What I ran: - New `tests/use-null-as-optional-test.ts`. It generates code for the schema from the issue and type checks it with the TypeScript API. Before the change, the compile test fails with the error above and the test for `{ view: null }` fails. After it, both pass. A third test checks that an unknown property is still rejected, and passes both ways. - `tests/` and the two integration folders above: 70 tests pass. - `tsc --noEmit` on `tsconfig.json` and `tests/tsconfig.json`, and on the three changed generated files, on TypeScript 5.9.3. - Prettier on `src/main.ts` and the new test. - Regenerating `proto-2.ts` and `proto-3.ts` from their committed `.bin` files gives output identical to this branch. What I did not run: - `yarn proto2ts` and `pbjs.sh`, because they need Docker and protoc. I edited the `Builtin` line in the three generated files by hand. `use-null-as-optional` has no `.bin`, so that file was not regenerated. - The rest of the integration tests and the full `yarn tsc:check`. - TypeScript 7.1, the version #1293 targeted.
stephenh
pushed a commit
that referenced
this pull request
Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
Every generated file that uses
useExactTypes(the default) fails to compile on TypeScript 7.1 (checked with7.1.0-betaand7.1.0-dev.20261020):TypeScript 5.9 and 7.0 accept the same code.
Cause
The conditional distributes over
P, so in the false branchPis a single union member, while the constraintI extends Pstill refers to the whole union. Since microsoft/TypeScript#64237 (merged for 7.1) the compiler distinguishes the distributedPfrom the non-distributed one, soI[K](withK extends keyof <distributed P>) is no longer considered a valid index intoI, andI[K]no longer satisfiesP[K].This is intentional rather than a beta bug: the old alias was unsound (
I extends Child | undefineddoes not guarantee thatIhas the keys ofChild), and that PR uses the same shape,type Issue<A, B extends A> = A extends unknown ? Show<A, B> : never, as its example of a newly reported error. microsoft/TypeScript#64673 and its fix #64307 concern a separate regression from the same change (identical generic signatures no longer relating) and do not cover this pattern; the latest 7.1 nightly still rejects it.Fix
Iis no longer constrained by the alias.fromPartial/createalready constrainI extends Exact<DeepPartial<T>, I>, so nothing changes for callers.PthatIactually has, which letsI[K & keyof I]type-check. This produces the same checks as before: for keys missing fromIthe oldI[K]resolved tounknown, i.e. no excess-property check for that key, andP &still contributes every property. Union fields (oneof=unions),Builtins,useUnknownForValueand$typebehave as before.Alternatives that were tried and rejected:
K extends keyof I ? I[K] : unknownbreaks the nestedChild.fromPartial(object.child)calls inside generated code (deferred conditional types no longer relate across the genericIs), and any(I & X)[K]form produces "circularly references itself" errors.Testing
@ts-expect-errorfor unknown properties at the top level, in nested messages, in repeated fields and insideoneofcases, plus compiling valid partials) inintegration/simple,integration/oneof-unionsandintegration/value-unknown.yarn proto2tsre-generated all integration output;yarn tsc:checkandyarn testpass on TypeScript 5.9.3.integration/**/*.tsandintegration/**/*-test.tstype-check on[email protected](with the removedesModuleInteropoption dropped from the tsconfig). The only remaining 7.1 errors are the pre-existingnew DataLoader(...)calls on a namespace import inbatching-with-contextandlower-case-svc-methods, which are unrelated to this change.