Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +31400 to +31401

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I measured this and couldn't find this to make any different for any project. Also, the result is node-independent so the per-node cache wouldnt fit this all too well. I could add a second type-based cache but I couldn't prove anyhow it would actually be worth it.

}
key := DiscriminatedContextualTypeKey{nodeId: ast.GetNodeId(node), typeId: contextualType.id}
if discriminated := c.discriminatedContextualTypes[key]; discriminated != nil {
return discriminated
Expand Down
3 changes: 3 additions & 0 deletions tsc/internal/checker/jsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 36 additions & 1 deletion tsc/internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = <Nullable value={1} />;
~~~~~
!!! suggestion TS6385: 'value' is deprecated.
!!! related TS2798 jsxAttributes.tsx:5:9: The declaration was marked as deprecated here.
const plainElement = <Plain value={1} />;
~~~~~
!!! suggestion TS6385: 'value' is deprecated.
!!! related TS2798 jsxAttributes.tsx:5:9: The declaration was marked as deprecated here.

Original file line number Diff line number Diff line change
@@ -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 = <Nullable value={1} />;
>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 = <Plain value={1} />;
>plainElement : Symbol(plainElement, Decl(jsxAttributes.tsx, 10, 5))
>Plain : Symbol(Plain, Decl(jsxAttributes.tsx, 7, 65))
>value : Symbol(value, Decl(jsxAttributes.tsx, 10, 27))

Original file line number Diff line number Diff line change
@@ -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 = <Nullable value={1} />;
>nullableElement : JSX.Element
><Nullable value={1} /> : JSX.Element
>Nullable : (props: Props | undefined) => JSX.Element
>value : number
>1 : 1

const plainElement = <Plain value={1} />;
>plainElement : JSX.Element
><Plain value={1} /> : JSX.Element
>Plain : (props: Props) => JSX.Element
>value : number
>1 : 1

Original file line number Diff line number Diff line change
@@ -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 = <Nullable value={1} />;
const plainElement = <Plain value={1} />;