diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go
index 10ddb3c0c88b4..81d398f0552c1 100644
--- a/tsc/internal/checker/checker.go
+++ b/tsc/internal/checker/checker.go
@@ -31397,6 +31397,9 @@ func (d *ObjectLiteralDiscriminator) matches(index int, t *Type) bool {
}
func (c *Checker) discriminateContextualTypeByObjectMembers(node *ast.Node, contextualType *Type) *Type {
+ if !isDiscriminableUnion(contextualType) {
+ return c.removeNonDiscriminableConstituents(contextualType)
+ }
key := DiscriminatedContextualTypeKey{nodeId: ast.GetNodeId(node), typeId: contextualType.id}
if discriminated := c.discriminatedContextualTypes[key]; discriminated != nil {
return discriminated
diff --git a/tsc/internal/checker/jsx.go b/tsc/internal/checker/jsx.go
index 7f35f796957cf..639a6f95bfb04 100644
--- a/tsc/internal/checker/jsx.go
+++ b/tsc/internal/checker/jsx.go
@@ -264,6 +264,9 @@ func (c *Checker) getContextualTypeForChildJsxExpression(node *ast.Node, child *
}
func (c *Checker) discriminateContextualTypeByJSXAttributes(node *ast.Node, contextualType *Type) *Type {
+ if !isDiscriminableUnion(contextualType) {
+ return c.removeNonDiscriminableConstituents(contextualType)
+ }
key := DiscriminatedContextualTypeKey{nodeId: ast.GetNodeId(node), typeId: contextualType.id}
if discriminated := c.discriminatedContextualTypes[key]; discriminated != nil {
return discriminated
diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go
index d3b6c86014498..b6ae4a921cce3 100644
--- a/tsc/internal/checker/relater.go
+++ b/tsc/internal/checker/relater.go
@@ -1051,9 +1051,44 @@ func (d *TypeDiscriminator) matches(index int, t *Type) bool {
return false
}
+// A union can only be narrowed by discriminants when it has at least two non-primitive constituents to
+// tell apart. This rules out the very common `T | undefined` shaped unions before any property work.
+func isDiscriminableUnion(t *Type) bool {
+ if t.objectFlags&ObjectFlagsPrimitiveUnion != 0 {
+ return false
+ }
+ count := 0
+ for _, s := range t.Types() {
+ if s.flags&TypeFlagsPrimitive == 0 {
+ count++
+ if count == 2 {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// Discriminating a union always drops its primitive and never constituents, even when there are no
+// discriminants to go by. Callers that skip the discrimination for unions that cannot be narrowed still
+// rely on that, for instance when looking up a contextual property, so this reproduces just that part.
+func (c *Checker) removeNonDiscriminableConstituents(t *Type) *Type {
+ types := t.Types()
+ filtered := make([]*Type, 0, len(types))
+ for _, s := range types {
+ if s.flags&TypeFlagsPrimitive == 0 && c.getReducedType(s).flags&TypeFlagsNever == 0 {
+ filtered = append(filtered, s)
+ }
+ }
+ if len(filtered) == 0 || len(filtered) == len(types) {
+ return t
+ }
+ return c.getUnionTypeEx(filtered, UnionReductionNone, nil, nil)
+}
+
// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
func (c *Checker) findMatchingDiscriminantType(source *Type, target *Type, isRelatedTo func(source *Type, target *Type) Ternary) *Type {
- if target.flags&TypeFlagsUnion != 0 && source.flags&(TypeFlagsIntersection|TypeFlagsObject) != 0 {
+ if target.flags&TypeFlagsUnion != 0 && source.flags&(TypeFlagsIntersection|TypeFlagsObject) != 0 && isDiscriminableUnion(target) {
if match := c.getMatchingUnionConstituentForType(target, source); match != nil {
return match
}
diff --git a/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.errors.txt b/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.errors.txt
new file mode 100644
index 0000000000000..981a5245f6ccd
--- /dev/null
+++ b/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.errors.txt
@@ -0,0 +1,39 @@
+jsxAttributes.tsx(10,35): suggestion TS6385: 'value' is deprecated.
+jsxAttributes.tsx(11,29): suggestion TS6385: 'value' is deprecated.
+objectLiteral.ts(5,41): suggestion TS6385: 'value' is deprecated.
+objectLiteral.ts(6,26): suggestion TS6385: 'value' is deprecated.
+
+
+==== objectLiteral.ts (2 errors) ====
+ interface Options {
+ /** @deprecated use other */
+ value: number;
+ }
+ const nullable: Options | undefined = { value: 1 };
+ ~~~~~
+!!! suggestion TS6385: 'value' is deprecated.
+!!! related TS2798 objectLiteral.ts:2:9: The declaration was marked as deprecated here.
+ const plain: Options = { value: 1 };
+ ~~~~~
+!!! suggestion TS6385: 'value' is deprecated.
+!!! related TS2798 objectLiteral.ts:2:9: The declaration was marked as deprecated here.
+
+==== jsxAttributes.tsx (2 errors) ====
+ declare namespace JSX {
+ interface Element {}
+ }
+ interface Props {
+ /** @deprecated use other */
+ value: number;
+ }
+ declare function Nullable(props: Props | undefined): JSX.Element;
+ declare function Plain(props: Props): JSX.Element;
+ const nullableElement = ;
+ ~~~~~
+!!! suggestion TS6385: 'value' is deprecated.
+!!! related TS2798 jsxAttributes.tsx:5:9: The declaration was marked as deprecated here.
+ const plainElement = ;
+ ~~~~~
+!!! suggestion TS6385: 'value' is deprecated.
+!!! related TS2798 jsxAttributes.tsx:5:9: The declaration was marked as deprecated here.
+
\ No newline at end of file
diff --git a/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.symbols b/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.symbols
new file mode 100644
index 0000000000000..8b49d638e2213
--- /dev/null
+++ b/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.symbols
@@ -0,0 +1,58 @@
+//// [tests/cases/compiler/deprecatedPropertyInNullableContextualType.ts] ////
+
+=== objectLiteral.ts ===
+interface Options {
+>Options : Symbol(Options, Decl(objectLiteral.ts, 0, 0))
+
+ /** @deprecated use other */
+ value: number;
+>value : Symbol(Options.value, Decl(objectLiteral.ts, 0, 19))
+}
+const nullable: Options | undefined = { value: 1 };
+>nullable : Symbol(nullable, Decl(objectLiteral.ts, 4, 5))
+>Options : Symbol(Options, Decl(objectLiteral.ts, 0, 0))
+>value : Symbol(value, Decl(objectLiteral.ts, 4, 39))
+
+const plain: Options = { value: 1 };
+>plain : Symbol(plain, Decl(objectLiteral.ts, 5, 5))
+>Options : Symbol(Options, Decl(objectLiteral.ts, 0, 0))
+>value : Symbol(value, Decl(objectLiteral.ts, 5, 24))
+
+=== jsxAttributes.tsx ===
+declare namespace JSX {
+>JSX : Symbol(JSX, Decl(jsxAttributes.tsx, 0, 0))
+
+ interface Element {}
+>Element : Symbol(Element, Decl(jsxAttributes.tsx, 0, 23))
+}
+interface Props {
+>Props : Symbol(Props, Decl(jsxAttributes.tsx, 2, 1))
+
+ /** @deprecated use other */
+ value: number;
+>value : Symbol(Props.value, Decl(jsxAttributes.tsx, 3, 17))
+}
+declare function Nullable(props: Props | undefined): JSX.Element;
+>Nullable : Symbol(Nullable, Decl(jsxAttributes.tsx, 6, 1))
+>props : Symbol(props, Decl(jsxAttributes.tsx, 7, 26))
+>Props : Symbol(Props, Decl(jsxAttributes.tsx, 2, 1))
+>JSX : Symbol(JSX, Decl(jsxAttributes.tsx, 0, 0))
+>Element : Symbol(JSX.Element, Decl(jsxAttributes.tsx, 0, 23))
+
+declare function Plain(props: Props): JSX.Element;
+>Plain : Symbol(Plain, Decl(jsxAttributes.tsx, 7, 65))
+>props : Symbol(props, Decl(jsxAttributes.tsx, 8, 23))
+>Props : Symbol(Props, Decl(jsxAttributes.tsx, 2, 1))
+>JSX : Symbol(JSX, Decl(jsxAttributes.tsx, 0, 0))
+>Element : Symbol(JSX.Element, Decl(jsxAttributes.tsx, 0, 23))
+
+const nullableElement = ;
+>nullableElement : Symbol(nullableElement, Decl(jsxAttributes.tsx, 9, 5))
+>Nullable : Symbol(Nullable, Decl(jsxAttributes.tsx, 6, 1))
+>value : Symbol(value, Decl(jsxAttributes.tsx, 9, 33))
+
+const plainElement = ;
+>plainElement : Symbol(plainElement, Decl(jsxAttributes.tsx, 10, 5))
+>Plain : Symbol(Plain, Decl(jsxAttributes.tsx, 7, 65))
+>value : Symbol(value, Decl(jsxAttributes.tsx, 10, 27))
+
diff --git a/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.types b/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.types
new file mode 100644
index 0000000000000..c43488ce07940
--- /dev/null
+++ b/tsc/testdata/baselines/reference/compiler/deprecatedPropertyInNullableContextualType.types
@@ -0,0 +1,53 @@
+//// [tests/cases/compiler/deprecatedPropertyInNullableContextualType.ts] ////
+
+=== objectLiteral.ts ===
+interface Options {
+ /** @deprecated use other */
+ value: number;
+>value : number
+}
+const nullable: Options | undefined = { value: 1 };
+>nullable : Options | undefined
+>{ value: 1 } : { value: number; }
+>value : number
+>1 : 1
+
+const plain: Options = { value: 1 };
+>plain : Options
+>{ value: 1 } : { value: number; }
+>value : number
+>1 : 1
+
+=== jsxAttributes.tsx ===
+declare namespace JSX {
+ interface Element {}
+}
+interface Props {
+ /** @deprecated use other */
+ value: number;
+>value : number
+}
+declare function Nullable(props: Props | undefined): JSX.Element;
+>Nullable : (props: Props | undefined) => JSX.Element
+>props : Props | undefined
+>JSX : any
+
+declare function Plain(props: Props): JSX.Element;
+>Plain : (props: Props) => JSX.Element
+>props : Props
+>JSX : any
+
+const nullableElement = ;
+>nullableElement : JSX.Element
+> : JSX.Element
+>Nullable : (props: Props | undefined) => JSX.Element
+>value : number
+>1 : 1
+
+const plainElement = ;
+>plainElement : JSX.Element
+> : JSX.Element
+>Plain : (props: Props) => JSX.Element
+>value : number
+>1 : 1
+
diff --git a/tsc/testdata/tests/cases/compiler/deprecatedPropertyInNullableContextualType.ts b/tsc/testdata/tests/cases/compiler/deprecatedPropertyInNullableContextualType.ts
new file mode 100644
index 0000000000000..f4a21011b0c7b
--- /dev/null
+++ b/tsc/testdata/tests/cases/compiler/deprecatedPropertyInNullableContextualType.ts
@@ -0,0 +1,25 @@
+// @strict: true
+// @jsx: preserve
+// @captureSuggestions: true
+// @noEmit: true
+
+// @filename: objectLiteral.ts
+interface Options {
+ /** @deprecated use other */
+ value: number;
+}
+const nullable: Options | undefined = { value: 1 };
+const plain: Options = { value: 1 };
+
+// @filename: jsxAttributes.tsx
+declare namespace JSX {
+ interface Element {}
+}
+interface Props {
+ /** @deprecated use other */
+ value: number;
+}
+declare function Nullable(props: Props | undefined): JSX.Element;
+declare function Plain(props: Props): JSX.Element;
+const nullableElement = ;
+const plainElement = ;