diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8850e83a808fc..833919e002d1b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11205,15 +11205,30 @@ namespace ts { return type.flags & TypeFlags.Undefined ? type : getUnionType([type, undefinedType]); } + function isUnconstrainedOrNulllUndefinedConstrainedTypeParameter(type: Type) { + if (!(type.flags & TypeFlags.Instantiable)) return false; + const constraint = getBaseConstraintOfType(type); + if (!constraint) return true; + return maybeTypeOfKind(constraint, TypeFlags.Undefined | TypeFlags.Null); + } + function getGlobalNonNullableTypeInstantiation(type: Type) { + const filtered = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); // If type alias unavailable, at least mimic non-higherorder behavior if (!deferredGlobalNonNullableTypeAlias) { deferredGlobalNonNullableTypeAlias = getGlobalSymbol("NonNullable" as __String, SymbolFlags.TypeAlias, /*diagnostic*/ undefined) || unknownSymbol; } // Use NonNullable global type alias if available to improve quick info/declaration emit if (deferredGlobalNonNullableTypeAlias !== unknownSymbol) { - return getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [type]); + // The below check prevents us from making a NonNullable where T can't ever be null or undefined + // This feels unnecessesary, as if `T extends Foo`, NonNullable is trivially just `T` (as no + // value of T could ever be nullable); but our conditional type logic is lacking here and can't make + // that leap (it stays generic to allow for, eg T to be instantiated with `never`, which extends `Foo` + // _and_ undefined, thereby choosing a different branch than it would simplify to). + if (forEachType(filtered, isUnconstrainedOrNulllUndefinedConstrainedTypeParameter)) { + return getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [filtered]); + } } - return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); // Type alias unavailable, fall back to non-higherorder behavior + return filtered; } function getNonNullableType(type: Type): Type { diff --git a/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.js b/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.js new file mode 100644 index 0000000000000..e2b8d1ae6130b --- /dev/null +++ b/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.js @@ -0,0 +1,38 @@ +//// [bangOperatorRemovesNonNullableWhereSensible.ts] +export class Entry { + private _table: T | null = null; + createSubsetForDirectory(): void { + const entry = new Entry(); + this._table!.fn(entry); + } +} + +export abstract class Table { + fn(directoryEntry: Entry): this | null { + return null; + } +} + +//// [bangOperatorRemovesNonNullableWhereSensible.js] +"use strict"; +exports.__esModule = true; +var Entry = /** @class */ (function () { + function Entry() { + this._table = null; + } + Entry.prototype.createSubsetForDirectory = function () { + var entry = new Entry(); + this._table.fn(entry); + }; + return Entry; +}()); +exports.Entry = Entry; +var Table = /** @class */ (function () { + function Table() { + } + Table.prototype.fn = function (directoryEntry) { + return null; + }; + return Table; +}()); +exports.Table = Table; diff --git a/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.symbols b/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.symbols new file mode 100644 index 0000000000000..7478c61d5992d --- /dev/null +++ b/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.symbols @@ -0,0 +1,39 @@ +=== tests/cases/compiler/bangOperatorRemovesNonNullableWhereSensible.ts === +export class Entry { +>Entry : Symbol(Entry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 0)) +>T : Symbol(T, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 19)) +>Table : Symbol(Table, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 6, 1)) + + private _table: T | null = null; +>_table : Symbol(Entry._table, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 37)) +>T : Symbol(T, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 19)) + + createSubsetForDirectory(): void { +>createSubsetForDirectory : Symbol(Entry.createSubsetForDirectory, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 1, 36)) + + const entry = new Entry(); +>entry : Symbol(entry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 3, 13)) +>Entry : Symbol(Entry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 0)) +>T : Symbol(T, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 19)) + + this._table!.fn(entry); +>this._table!.fn : Symbol(Table.fn, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 8, 29)) +>this._table : Symbol(Entry._table, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 37)) +>this : Symbol(Entry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 0)) +>_table : Symbol(Entry._table, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 37)) +>fn : Symbol(Table.fn, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 8, 29)) +>entry : Symbol(entry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 3, 13)) + } +} + +export abstract class Table { +>Table : Symbol(Table, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 6, 1)) + + fn(directoryEntry: Entry): this | null { +>fn : Symbol(Table.fn, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 8, 29)) +>directoryEntry : Symbol(directoryEntry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 9, 7)) +>Entry : Symbol(Entry, Decl(bangOperatorRemovesNonNullableWhereSensible.ts, 0, 0)) + + return null; + } +} diff --git a/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.types b/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.types new file mode 100644 index 0000000000000..c05cc1bdae9ea --- /dev/null +++ b/tests/baselines/reference/bangOperatorRemovesNonNullableWhereSensible.types @@ -0,0 +1,46 @@ +=== tests/cases/compiler/bangOperatorRemovesNonNullableWhereSensible.ts === +export class Entry { +>Entry : Entry +>T : T +>Table : Table + + private _table: T | null = null; +>_table : T | null +>T : T +>null : null +>null : null + + createSubsetForDirectory(): void { +>createSubsetForDirectory : () => void + + const entry = new Entry(); +>entry : Entry +>new Entry() : Entry +>Entry : typeof Entry +>T : T + + this._table!.fn(entry); +>this._table!.fn(entry) : T | null +>this._table!.fn : (directoryEntry: Entry) => T | null +>this._table! : T +>this._table : T | null +>this : this +>_table : T | null +>fn : (directoryEntry: Entry) => T | null +>entry : Entry + } +} + +export abstract class Table { +>Table : Table + + fn(directoryEntry: Entry): this | null { +>fn : (directoryEntry: Entry) => this | null +>directoryEntry : Entry +>Entry : Entry +>null : null + + return null; +>null : null + } +} diff --git a/tests/baselines/reference/nonNullParameterExtendingStringAssignableToString.types b/tests/baselines/reference/nonNullParameterExtendingStringAssignableToString.types index 8590db628d0df..9304ead67637b 100644 --- a/tests/baselines/reference/nonNullParameterExtendingStringAssignableToString.types +++ b/tests/baselines/reference/nonNullParameterExtendingStringAssignableToString.types @@ -29,7 +29,7 @@ function fn(one: T, two: U) { foo(two!); >foo(two!) : void >foo : (p: string) => void ->two! : NonNullable +>two! : U >two : U foo(three!); // this line is the important one diff --git a/tests/cases/compiler/bangOperatorRemovesNonNullableWhereSensible.ts b/tests/cases/compiler/bangOperatorRemovesNonNullableWhereSensible.ts new file mode 100644 index 0000000000000..9f12d41ccb40a --- /dev/null +++ b/tests/cases/compiler/bangOperatorRemovesNonNullableWhereSensible.ts @@ -0,0 +1,14 @@ +// @strict: true +export class Entry { + private _table: T | null = null; + createSubsetForDirectory(): void { + const entry = new Entry(); + this._table!.fn(entry); + } +} + +export abstract class Table { + fn(directoryEntry: Entry): this | null { + return null; + } +} \ No newline at end of file