diff --git a/.chronus/changes/fix-optional-prop-required-2024-4-6-21-39-1.md b/.chronus/changes/fix-optional-prop-required-2024-4-6-21-39-1.md new file mode 100644 index 0000000000..5d92ac5e75 --- /dev/null +++ b/.chronus/changes/fix-optional-prop-required-2024-4-6-21-39-1.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Model with an optional property should not be satisfy a constraint with that property required. (`{foo?: string}` cannot be assigned to a constraint of `{foo: string}`) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index a693f612f8..9af59b7d52 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -5785,6 +5785,18 @@ export function createChecker(program: Program): Checker { } else { remainingProperties.delete(prop.name); + if (sourceProperty.optional && !prop.optional) { + diagnostics.push( + createDiagnostic({ + code: "property-required", + format: { + propName: prop.name, + targetType: getTypeName(target), + }, + target: diagnosticTarget, + }) + ); + } const [related, propDiagnostics] = isTypeAssignableToInternal( sourceProperty.type, prop.type, diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index b7311898ed..51fa9ffcda 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -404,6 +404,12 @@ const diagnostics = { withDetails: paramMessage`Type '${"sourceType"}' is not assignable to type '${"targetType"}'\n ${"details"}`, }, }, + "property-required": { + severity: "error", + messages: { + default: paramMessage`Property '${"propName"}' is required in type '${"targetType"}' but here is optional.`, + }, + }, "no-prop": { severity: "error", messages: { diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts index ba66144a2a..20b13217a5 100644 --- a/packages/compiler/test/checker/relation.test.ts +++ b/packages/compiler/test/checker/relation.test.ts @@ -737,6 +737,16 @@ describe("compiler: checker: type relations", () => { }); }); + it("emit diagnostic when optional property is assigned to required", async () => { + await expectTypeNotAssignable( + { source: `{foo?: string}`, target: `{foo: string}` }, + { + code: "property-required", + message: "Property 'foo' is required in type '(anonymous model)' but here is optional.", + } + ); + }); + it("emit diagnostic when required property is missing", async () => { await expectTypeNotAssignable( { source: `{foo: "abc"}`, target: `{foo: string, bar: string}` },