fix: support exactOptionalPropertyTypes - #4087
Conversation
Enable exactOptionalPropertyTypes in the repo tsconfig and adjust route schema / OpenAPI option types so TypeBox and plain JSON Schema remain usable. Optional inferred object fields also accept explicit undefined. Closes #402
📝 WalkthroughSummary by CodeRabbit
WalkthroughTypeScript ChangesExact optional property type support
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: dependency version conflict. Check your lock file or package.json. 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. Comment |
💻 Website PreviewThe latest changes are available as preview in: https://pr-4087.fets-3ku.pages.dev |
✅ Benchmark Results |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/fets/src/plugins/openapi.ts`:
- Around line 19-27: Update the defaulted properties in SwaggerUIOpts, including
spec and the related display/model options, to reject explicit undefined under
exactOptionalPropertyTypes by removing their | undefined unions; preserve
optional omission while keeping the existing plugin defaults and spread behavior
unchanged.
In `@packages/fets/src/types.ts`:
- Around line 342-348: Update the mapped type in AddUndefToOptionals so each
property value is recursively transformed with AddUndefToOptionals before
applying the optional-property undefined widening. Preserve the existing object
and readonly-array handling while ensuring nested optional objects and their
descendants accept undefined under exactOptionalPropertyTypes.
🪄 Autofix
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: CHILL
Plan: Pro Plus
Run ID: bdd2fcca-48d0-4b10-8721-673d0b1204f0
📒 Files selected for processing (5)
.changeset/exact-optional-property-types.mdpackages/fets/src/plugins/openapi.tspackages/fets/src/typed-fetch.tspackages/fets/src/types.tstsconfig.json
| spec?: OpenAPIDocument | undefined; | ||
| dom_id?: string | undefined; | ||
| displayOperationId?: boolean | undefined; | ||
| tryItOutEnabled?: boolean | undefined; | ||
| requestSnippetsEnabled?: boolean | undefined; | ||
| displayRequestDuration?: boolean | undefined; | ||
| defaultModelRendering?: 'model' | 'example' | 'schema' | undefined; | ||
| defaultModelExpandDepth?: number | undefined; | ||
| defaultModelsExpandDepth?: number | undefined; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Confirm that the defaulted options are assigned before swaggerUIOpts is spread.
rg -n -C 12 'spec: router\.openAPIDocument|swaggerUIOpts' packages/fets/src/plugins/openapi.ts
# Locate OpenAPI or Swagger UI tests that should cover explicit undefined values.
fd -t f . packages/fets | rg -i '(openapi|swagger).*(test|spec)|(test|spec).*(openapi|swagger)' || trueRepository: ardatan/feTS
Length of output: 3008
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SwaggerUIOpts type =="
sed -n '25,44p' packages/fets/src/plugins/openapi.ts | cat -n
echo
echo "== relevant openapi tests =="
sed -n '1,240p' packages/fets/tests/plugins/openapi.spec.ts | cat -n
echo
echo "== tsconfig exactOptionalPropertyTypes =="
fd -t f 'tsconfig.*json' . -x sh -c 'echo "--- {}"; grep -n "exactOptionalPropertyTypes" "{}" || true'Repository: ardatan/feTS
Length of output: 5343
Keep defaulted Swagger UI options from accepting explicit undefined.
With exactOptionalPropertyTypes: true, { spec: undefined } is permitted. The handler spreads swaggerUIOpts after the defaults in JSON.stringify, so explicit undefined overwrites the plugin default before serialization and leaves the option out of the Swagger UI configuration. Remove | undefined from the defaulted SwaggerUIOpts properties, or strip undefined values before the spread.
🤖 Prompt for 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.
In `@packages/fets/src/plugins/openapi.ts` around lines 19 - 27, Update the
defaulted properties in SwaggerUIOpts, including spec and the related
display/model options, to reject explicit undefined under
exactOptionalPropertyTypes by removing their | undefined unions; preserve
optional omission while keeping the existing plugin defaults and spread behavior
unchanged.
| type AddUndefToOptionals<T> = T extends any | ||
| ? T extends object | ||
| ? T extends readonly any[] | ||
| ? T | ||
| : { | ||
| [K in keyof T]: {} extends Pick<T, K> ? T[K] | undefined : T[K]; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files matching types.ts:\n'
fd -a 'types\.ts$' . | sed 's#^\./##' | head -50
printf '\nRelevant section in packages/fets/src/types.ts:\n'
sed -n '310,380p' packages/fets/src/types.ts
printf '\nSearch FromSchemaOriginal and AddUndefToOptionals:\n'
rg -n "FromSchemaOriginal|AddUndefToOptionals|exactOptionalPropertyTypes" .Repository: ardatan/feTS
Length of output: 3555
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Section around FromSchemaResult and FromSchema:\n'
sed -n '350,405p' packages/fets/src/types.ts
printf '\nTypeScript compiler availability:\n'
if command -v tsc >/dev/null 2>&1; then
tsc --version
else
printf 'tsc not in PATH\n'
npm_exec="$(command -v npm-exec >/dev/null 2>&1 && echo npm-exec || true)"
if [ -n "$npm_exec" ]; then npm-exec tsc --version; else printf 'npm-exec tsc unavailable\n'; fi
fi
printf '\nMinimal TypeScript behavior probe for current and proposed mapping:\n'
python3 - <<'PY'
from pathlib import Path
p = Path('packages/fets/src/types.ts')
print('packages/fets/src/types.ts exists:', p.exists())
text = p.read_text()
needle = 'type AddUndefToOptionals<T> = T extends any'
idx = text.find(needle)
if idx == -1:
raise SystemExit(1)
end = text.find('type FromSchemaResult<T>', idx)
print(text[idx:end])
print('add_undef_to_optionals_defined', idx != -1)
print('FromSchemaResult_defined', text.find('type FromSchemaResult<T>', idx) != -1)
print('FromSchema_applies_current_type', 'export type FromSchema<T> = AddUndefToOptionals<FromSchemaResult<T>>;' in text)
PYRepository: ardatan/feTS
Length of output: 2216
🌐 Web query:
TypeScript exactOptionalPropertyTypes optional property undefined explicit assignability documented
💡 Result:
The exactOptionalPropertyTypes compiler option in TypeScript provides stricter, more predictable behavior for optional object properties [1][2]. When exactOptionalPropertyTypes is enabled (set to true), TypeScript enforces a clear distinction between a property that is absent from an object and a property that is present but set to undefined [1][3]. Key implications include: 1. Explicit Assignability: Under this setting, an optional property defined as prop?: T will not accept undefined as a value [1][4]. Attempting to explicitly assign undefined to such a property will result in a compiler error (e.g., "Type 'undefined' is not assignable to type 'T'") [1][5]. 2. Distinguishing Absence from Undefined: The flag enforces that "optional" means the property can be omitted entirely, but if the property is present, it must conform to the declared type [3][4]. This is distinct from standard behavior where optional properties are treated as T | undefined, implicitly allowing undefined even if it was not explicitly included in the type definition [3][6]. 3. Requirement for Explicit Union: If your code requires the ability to explicitly assign undefined to an optional property, you must update the type definition to include it, such as prop?: T | undefined [1][3][4]. This feature is not part of the standard strict mode suite because it is considered a significant breaking change for existing codebases that relied on the traditional behavior of allowing undefined in optional properties [6]. Using this flag helps catch bugs related to object shape mismatches and improves the accuracy of type contracts [3][4]. Top results: [1][3][6][4]
Citations:
- 1: https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html
- 2: https://www.typescriptlang.org/tsconfig/
- 3: https://jsmanifest.com/typescript-exactoptionalpropertytypes-strict-flag
- 4: https://www.codefixeshub.com/typescript/configuring-exact-optional-property-types-exactopt
- 5: Fix unassignable properties by adding undefined with exactOptionalPropertyTypes microsoft/TypeScript#45032
- 6: https://stackoverflow.com/questions/78029607/type-inference-can-be-erroneous-for-optional-properties-in-merged-objects-e-g
🌐 Web query:
TypeScript exactOptionalPropertyTypes explicit undefined property optional value type optional prop type assignment
💡 Result:
The exactOptionalPropertyTypes compiler option in TypeScript provides stricter handling for optional properties by distinguishing between a property being entirely absent from an object and a property being explicitly set to undefined [1][2]. Standard Behavior (Default) When exactOptionalPropertyTypes is disabled (the default), TypeScript treats an optional property declared as prop?: T as having the type T | undefined [1][3]. This means you can assign undefined to the property even if it is not explicitly included in the type definition [2][4]. In this state, the compiler considers the following three scenarios valid for prop?: T: 1. The property is absent from the object [1][2]. 2. The property is present with a value of type T [1][2]. 3. The property is present with the value undefined [1][2]. Behavior with exactOptionalPropertyTypes: true When this flag is enabled, TypeScript enforces that optional properties only accept the declared type T or complete absence [1][2]. You cannot explicitly assign undefined to the property unless undefined is part of the explicit type union [1][2][4]. For example: interface User { name?: string; } // Allowed (property is absent) const u1: User = {}; // Allowed (property has value of type string) const u2: User = { name: "Alice" }; // Error when exactOptionalPropertyTypes: true // Type 'undefined' is not assignable to type 'string' const u3: User = { name: undefined }; To allow undefined as a valid value while still maintaining an optional property, you must explicitly include it in the type definition: interface User { name?: string | undefined; } // Now allowed with exactOptionalPropertyTypes: true const u4: User = { name: undefined }; Why it is not in 'strict' This option is not included in the standard strict flag suite because it is a significant breaking change for existing codebases [5][6]. Many existing type definitions and utility functions historically relied on the assumption that optional properties could accept undefined [5][7]. Enabling this flag requires a developer to audit and potentially update type definitions where explicit undefined assignments were previously allowed [5]. Summary of Key Differences - Read type: Even with exactOptionalPropertyTypes enabled, reading from an optional property will still result in a type of T | undefined, because the property might be missing at runtime [5]. - Write/Assignment type: The restriction primarily affects assignment. It prevents developers from explicitly setting a property to undefined when the type definition does not explicitly allow it [1][5]. This helps catch bugs where the distinction between "missing" and "undefined" is important for downstream logic (e.g., when using Object.keys or the 'in' operator) [1][6].
Citations:
- 1: https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html
- 2: https://jsmanifest.com/typescript-exactoptionalpropertytypes-strict-flag
- 3: https://www.typescriptlang.org/play/4-4/new-ts-features/exact-optional-properties.ts.html
- 4: https://stackoverflow.com/questions/53371363/difference-between-typescript-optional-type-and-type-undefined
- 5: https://stackoverflow.com/questions/78029607/type-inference-can-be-erroneous-for-optional-properties-in-merged-objects-e-g
- 6: TS "Optional Properties" accepts "undefined" as a value when it shouldn't... microsoft/TypeScript#57479
- 7: Incorrect type inferred when using spread operator to override objects with optional properties. microsoft/TypeScript#57408
🌐 Web query:
TypeScript mapped type recursive conditional does not apply to value type exactOptionalPropertyTypes optional undefined test
💡 Result:
The behavior you are encountering is rooted in how TypeScript handles optional properties when exactOptionalPropertyTypes is enabled, combined with the way mapped types and recursive conditional types process property types [1][2]. When exactOptionalPropertyTypes is active, an optional property p?: T is strictly interpreted as T (the property may be missing) and not T | undefined [3]. However, the compiler's internal handling of these properties within mapped types can lead to counterintuitive results because the "missing" state of a property is distinct from the type undefined [1]. Key technical reasons for this behavior include: 1. Distinct Types of 'Undefined': When exactOptionalPropertyTypes is enabled, there are effectively two "flavors" of undefined: one representing the explicit value undefined and another representing a "missing" property [1]. Mapped types may struggle to preserve the "missing" state correctly during recursive operations because the index access T[P] can "decay" the missing property into an explicit undefined or force an unexpected union [1]. 2. Homomorphic Mappings: Mapped types that are homomorphic (using [P in keyof T]) are designed to preserve optionality modifiers (?) [4]. When you introduce a recursive conditional type (e.g., T[P] extends object ? ... : ...), you often break this homomorphic property [4]. If the mapping is no longer homomorphic, TypeScript can no longer reliably copy the optionality modifier or respect the exact optionality constraints of the original type [4][5]. 3. Relationship Checks and Assignability: Recent updates (e.g., PR #60890) have addressed issues where homomorphic mappings broke assignability under exactOptionalPropertyTypes [6]. However, complex recursive utilities still frequently face issues where the "missingness" of a property is lost or transformed because the conditional logic inside the mapped type processes the type as T | undefined (or just T) instead of maintaining the original optional property descriptor [1][7]. To work around this, ensure your mapped type remains homomorphic whenever possible [4][5]. If you must use conditional logic, try to perform key filtering in the as clause (key remapping) rather than the in clause to maintain the homomorphic structure, which helps the compiler track the original property modifiers more accurately [8][4]. If you are experiencing unexpected | undefined behavior, note that this is a known area of complexity where the compiler's display of the type and its underlying behavior can diverge [1][7].
Citations:
- 1: How does indexing an optional property within a mapped type behave? microsoft/TypeScript#60717
- 2: https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html
- 3: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html
- 4: https://stackoverflow.com/questions/79134552/preserve-field-optional-ness-using-conditional-and-mapped-type
- 5: Certain homomorphic mappings break assignability with exactOptionalPropertyTypes microsoft/TypeScript#60233
- 6: Improve relationship check for mapped type sources under
exactOptionalPropertyTypesmicrosoft/TypeScript#60890 - 7: Declaration emit change in 5.5 with
exactOptionalPropertyTypesmicrosoft/TypeScript#59056 - 8: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
🌐 Web query:
TypeScript exactOptionalPropertyTypes mapped type conditional T[K] property value undefined explicit assignment union
💡 Result:
When exactOptionalPropertyTypes is enabled, TypeScript distinguishes between a property being missing and a property being present with an undefined value [1]. This significantly affects how mapped types interact with optional properties and how index access types (like T[K]) are evaluated [2][3]. Under exactOptionalPropertyTypes: true: 1. Explicit Assignment: You cannot explicitly assign undefined to an optional property (e.g., obj.prop = undefined) unless undefined is explicitly included in the property's type definition (e.g., prop?: string | undefined) [1]. 2. Index Access Types (T[K]): When you read an optional property via T[K], the resulting type includes undefined [2][3]. This is because the property can be missing in the object, and reading a missing property yields undefined [2]. This behavior persists even though you cannot assign undefined back to that property [2]. 3. Mapped Types: Homomorphic mapped types (of the form { [K in keyof T]:... }) generally preserve optionality, but they can be sensitive to how they handle the T[K] values [3][4][5]. If you manipulate T[K] using conditional types—for example, to strip undefined from a union—the resulting mapped type may lose its homomorphic nature, causing it to no longer preserve property modifiers (like optionality) [4][5]. 4. Known Issues and Complexity: There are ongoing challenges in TypeScript regarding the consistency of type display and assignability when using mapped types with this flag [3][4][6]. For instance, mapped types that do not exactly mirror the source shape can sometimes create "decay" where the "missing" state of an optional property is conflated with an explicit undefined [3]. When building utilities to manipulate these properties, developers often have to use specific techniques like key remapping (using the as clause) to maintain homomorphic behavior and prevent accidental stripping of modifiers [4][5]. In summary, exactOptionalPropertyTypes restricts writing undefined to optional properties, but it does not remove undefined from the read-only type T[K], as undefined remains the runtime result of accessing a missing property [1][2].
Citations:
- 1: https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html
- 2: exactOptionalPropertyTypes being inconsistent with T[keyof T] microsoft/TypeScript#47587
- 3: How does indexing an optional property within a mapped type behave? microsoft/TypeScript#60717
- 4: Certain homomorphic mappings break assignability with exactOptionalPropertyTypes microsoft/TypeScript#60233
- 5: https://stackoverflow.com/questions/79134552/preserve-field-optional-ness-using-conditional-and-mapped-type
- 6: Generic indexed access on type with optional properties doesn't include
undefinedin its constraint inexactOptionalPropertyTypesmicrosoft/TypeScript#57023
🌐 Web query:
TypeScript exactOptionalPropertyTypes {} extends Pick<T K> optional property modifier T[K] explicit undefined
💡 Result:
When the TypeScript compiler option exactOptionalPropertyTypes is enabled, it enforces a stricter rule for optional properties: they cannot be explicitly assigned the value undefined unless undefined is explicitly included in the property's type definition (e.g., prop?: string | undefined) [1][2]. The behavior of Pick<T, K> and indexed access types (T[K]) under this setting is nuanced: 1. Indexing (T[K]): Even with exactOptionalPropertyTypes enabled, the type resulting from T[K] for an optional property K will typically include undefined [3]. This is because T[K] represents the type you get when reading from an object; if the property is missing, the value is undefined [3]. Consequently, even though the compiler prevents you from writing undefined to an optional property, it allows you to read undefined from it [3]. 2. Utility Types like Pick<T, K>: Pick<T, K> preserves the optionality of properties as defined in the source type T [4]. If T has an optional property prop?: string, then Pick<T, 'prop'> will also have prop?: string. Because the optional property modifier ? remains, the constraint imposed by exactOptionalPropertyTypes continues to apply to the resulting type [1][2]. 3. Interaction: There is a known distinction in TypeScript between the "write" type and the "read" type of optional properties when exactOptionalPropertyTypes is active [3][5]. The compiler effectively tracks whether a property is allowed to hold the value undefined [6]. While Pick creates a type that mirrors the original structure, the compiler enforces the restriction against assigning undefined to optional properties regardless of how the type was constructed, provided the property was marked optional in the resulting type [1][4]. In summary, exactOptionalPropertyTypes restricts the assignment of undefined to optional properties [1][3]. While indexed access types (T[K]) retain undefined in their type to account for the possibility of a property being missing at runtime, the compiler uses the optional modifier to prevent the explicit assignment of undefined at compile time [3][6].
Citations:
- 1: https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html
- 2: https://www.typescriptlang.org/play/4-4/new-ts-features/exact-optional-properties.ts.html
- 3: exactOptionalPropertyTypes being inconsistent with T[keyof T] microsoft/TypeScript#47587
- 4: https://stackoverflow.com/questions/73864636/how-to-differentiate-between-a-mapped-type-with-pick-and-the-original-type-when
- 5: Generic indexed access on type with optional properties doesn't include
undefinedin its constraint inexactOptionalPropertyTypesmicrosoft/TypeScript#57023 - 6: How does indexing an optional property within a mapped type behave? microsoft/TypeScript#60717
🌐 Web query:
TypeScript {} extends Pick<T K> exactOptionalPropertyTypes optional property
💡 Result:
The conditional type {} extends Pick<T, K> is a common TypeScript technique used to detect whether a property K in type T is optional [1][2]. This pattern relies on the fact that an empty object {} is assignable to an object type if all of its properties are optional [3][2]. When exactOptionalPropertyTypes is enabled, the behavior of optional properties becomes stricter: they no longer implicitly accept undefined as a valid value unless explicitly included in the type (e.g., prop?: string | undefined) [4][5]. ### How the Pattern Works - Pick<T, K> creates an object type containing only the property K from T [2]. - If property K is optional (i.e., K?: V), then Pick<T, K> effectively becomes { K?: V }. - Because {} is assignable to any object type where all properties are optional, {} extends Pick<T, K> evaluates to true when K is optional [3][2]. - If property K is required (i.e., K: V), Pick<T, K> becomes { K: V }. The empty object {} cannot satisfy this, so the expression evaluates to false [2]. ### Interaction with exactOptionalPropertyTypes While this pattern is primarily designed to identify the optionality modifier (?), its reliability can be affected by the exactOptionalPropertyTypes flag [6][7]. 1. Distinguishing Optionality: The pattern successfully distinguishes between required properties and those marked with the ? modifier, regardless of whether exactOptionalPropertyTypes is enabled [6]. 2. undefined Handling: The flag changes whether undefined is a valid assignment for an optional property, but it does not change the fact that an optional property is structurally "missing" by default [4][5]. The check {} extends Pick<T, K> remains a structural test for whether an object can be constructed without that key [3][8]. 3. Edge Cases: If you rely on this pattern to infer types, be aware that properties explicitly typed as prop: V | undefined (without the ? modifier) are required, and the check will correctly identify them as such (evaluating to false) [2]. Conversely, if a property is prop?: V | undefined, the check evaluates to true because of the ? modifier [9]. Developers often use this to create utility types, such as extracting optional or required keys from an interface [1][3][10]: typescript // Extracts keys that are optional type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T]; In summary, the pattern works as a structural check for the ? modifier, which persists even when exactOptionalPropertyTypes is enabled to enforce stricter assignment rules [6][4].
Citations:
- 1: https://blog.beraliv.dev/2021-12-07-get-optional
- 2: https://sametcc.me/gist/typescript-types
- 3: https://zenn.dev/axoloto210/articles/nextjs-util-type?locale=en
- 4: https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html
- 5:
exactOptionalPropertyTypesfaults the use ofOmitmicrosoft/TypeScript#60138 - 6: https://stackoverflow.com/questions/69034939/construct-new-type-with-required-properties-from-the-optional-properties-of-anot
- 7: exactOptionalPropertyTypes being inconsistent with T[keyof T] microsoft/TypeScript#47587
- 8: https://stackoverflow.com/questions/73864636/how-to-differentiate-between-a-mapped-type-with-pick-and-the-original-type-when
- 9: Removing optional modifier in a mapped type behaves inconsistently b/w array vs object microsoft/TypeScript#63291
- 10: https://stackoverflow.com/questions/70843199/is-there-a-way-to-define-a-required-type-for-just-optional-properties-defaults
Make AddUndefToOptionals recursive.
AddUndefToOptionals currently widens only the optional modifier on the direct object; nested optional object properties keep their original value type. Under exactOptionalPropertyTypes, passing undefined for an inner optional field then rejects unless each property value is transformed recursively.
🤖 Prompt for 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.
In `@packages/fets/src/types.ts` around lines 342 - 348, Update the mapped type in
AddUndefToOptionals so each property value is recursively transformed with
AddUndefToOptionals before applying the optional-property undefined widening.
Preserve the existing object and readonly-array handling while ensuring nested
optional objects and their descendants accept undefined under
exactOptionalPropertyTypes.
There was a problem hiding this comment.
Pull request overview
Enables TypeScript’s exactOptionalPropertyTypes across the repo and updates the fets public type surface (route schemas, OpenAPI/Swagger options, and schema-derived request/response types) to remain usable under EOPT semantics.
Changes:
- Turn on
exactOptionalPropertyTypesintsconfig.json. - Broaden route-schema input typing (supporting TypeBox + plain JSON Schema) and widen optional fields to accept explicit
undefinedwhere needed. - Update OpenAPI/Swagger/router option bag types to tolerate omitted vs explicitly-
undefinedvalues under EOPT; add a changeset entry.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Enables exactOptionalPropertyTypes. |
| packages/fets/src/types.ts | Introduces RouteSchema, widens option bags with ` |
| packages/fets/src/typed-fetch.ts | Loosens typed request/body/header generic constraints to work with EOPT-friendly object maps. |
| packages/fets/src/plugins/openapi.ts | Updates Swagger UI options typing for EOPT (` |
| .changeset/exact-optional-property-types.md | Adds a patch changeset describing EOPT support changes. |
Suppressed comments (3)
packages/fets/src/typed-fetch.ts:83
TypedHeadersis meant to reflect the FetchHeadersAPI, which always stores/returns string values. WithTMap extends object, callers can supply non-string value types and TypeScript will incorrectly type.get()/.entries()as returning those non-string values.
Consider constraining TMap values back to string | undefined (this still works with exact-optional property types).
export interface TypedHeaders<TMap extends object = Record<string, string | undefined>> {
append<TName extends DefaultHTTPHeaders | keyof TMap>(
name: TName,
value: TName extends keyof TMap ? TMap[TName] : string,
): void;
packages/fets/src/typed-fetch.ts:312
TypedRequestInitcurrently allowsTHeadersto be anyobject, which permits non-string header value types while still flowing intoTypedHeaders<THeaders>.
Tighten the constraint to Record<string, string | undefined> so header value types stay aligned with the actual Fetch API.
export type TypedRequestInit<
THeaders extends object,
TMethod extends HTTPMethod,
TFormData extends object,
> = Omit<RequestInit, 'method' | 'headers' | 'body'> & {
method: TMethod;
packages/fets/src/typed-fetch.ts:337
TypedRequest/TypedRequestCtoruseTHeaders extends object, which allows non-string header value types while still flowing intoTypedHeaders<THeaders>. Since FetchHeadersalways stores/returns strings, this makes request header typings unsound.
Constrain THeaders to Record<string, string | undefined> here as well so type parameters remain consistent and aligned with runtime behavior.
export type TypedRequest<
TJSON = any,
// `object` (not `Record<…| undefined>`) so EOPT optional fields like
// `{ description?: string; file: File }` remain valid form/header maps.
TFormData extends object = Record<string, FormDataEntryValue | undefined>,
THeaders extends object = Record<string, string | undefined>,
TMethod extends HTTPMethod = HTTPMethod,
TQueryParams = any,
TPathParams extends Record<string, any> = Record<string, any>,
> = Omit<Request, 'json' | 'method' | 'headers' | 'formData'> &
TypedBody<TJSON, TFormData, THeaders> & {
parsedUrl: URL;
method: TMethod;
params: TPathParams;
query: TQueryParams;
};
export type TypedRequestCtor = new <
THeaders extends object,
TMethod extends HTTPMethod,
TFormData extends object,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export type TypedBody<TJSON, TFormData extends object, THeaders extends object> = Omit< | ||
| Body, | ||
| 'json' | 'formData' | 'headers' | ||
| > & { |
48acead to
94e7115
Compare
Summary
exactOptionalPropertyTypesin the repotsconfigand close #402json-schema-to-ts'sJSONSchema)| undefinedwhere values may be omitted or explicitly undefinedundefined(including in circularDirectTypegraphs)Test plan
npm run ts:checkpasses withexactOptionalPropertyTypes: true