Skip to content

Commit

Permalink
Properly remove generic types that are constrained to 'null | undefin…
Browse files Browse the repository at this point in the history
…ed' in getNonNullableType (#44219)

* Improve getNonNullableType function

* Add tests

* More closely match previous behavior

* Add non-strict mode test
  • Loading branch information
ahejlsberg committed May 25, 2021
1 parent 52cefdf commit 3938958
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 6 deletions.
15 changes: 9 additions & 6 deletions src/compiler/checker.ts
Expand Up @@ -20325,14 +20325,17 @@ namespace ts {
}

function getGlobalNonNullableTypeInstantiation(type: Type) {
// First reduce away any constituents that are assignable to 'undefined' or 'null'. This not only eliminates
// 'undefined' and 'null', but also higher-order types such as a type parameter 'U extends undefined | null'
// that isn't eliminated by a NonNullable<T> instantiation.
const reducedType = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
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]);
}
return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); // Type alias unavailable, fall back to non-higher-order behavior
// If the NonNullable<T> type is available, return an instantiation. Otherwise just return the reduced type.
return deferredGlobalNonNullableTypeAlias !== unknownSymbol ?
getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [reducedType]) :
reducedType;
}

function getNonNullableType(type: Type): Type {
Expand Down Expand Up @@ -24124,7 +24127,7 @@ namespace ts {
}

function isGenericTypeWithUnionConstraint(type: Type) {
return !!(type.flags & TypeFlags.Instantiable && getBaseConstraintOrType(type).flags & TypeFlags.Union);
return !!(type.flags & TypeFlags.Instantiable && getBaseConstraintOrType(type).flags & (TypeFlags.Nullable | TypeFlags.Union));
}

function containsGenericType(type: Type): boolean {
Expand Down
33 changes: 33 additions & 0 deletions tests/baselines/reference/nonNullableReduction.js
@@ -0,0 +1,33 @@
//// [nonNullableReduction.ts]
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
f1?.("hello");
f2?.("hello");
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
let z = x!; // NonNullable<T>
}

function f2<T, U extends null | undefined>(x: T | U) {
let z = x!; // NonNullable<T>
}


//// [nonNullableReduction.js]
"use strict";
// Repros from #43425
function test(f1, f2) {
f1 === null || f1 === void 0 ? void 0 : f1("hello");
f2 === null || f2 === void 0 ? void 0 : f2("hello");
}
function f1(x) {
var z = x; // NonNullable<T>
}
function f2(x) {
var z = x; // NonNullable<T>
}
61 changes: 61 additions & 0 deletions tests/baselines/reference/nonNullableReduction.symbols
@@ -0,0 +1,61 @@
=== tests/cases/compiler/nonNullableReduction.ts ===
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
>Transform1 : Symbol(Transform1, Decl(nonNullableReduction.ts, 0, 0))
>T : Symbol(T, Decl(nonNullableReduction.ts, 2, 16))
>value : Symbol(value, Decl(nonNullableReduction.ts, 2, 23))
>T : Symbol(T, Decl(nonNullableReduction.ts, 2, 16))
>T : Symbol(T, Decl(nonNullableReduction.ts, 2, 16))

type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;
>Transform2 : Symbol(Transform2, Decl(nonNullableReduction.ts, 2, 85))
>T : Symbol(T, Decl(nonNullableReduction.ts, 3, 16))
>T : Symbol(T, Decl(nonNullableReduction.ts, 3, 16))
>value : Symbol(value, Decl(nonNullableReduction.ts, 3, 42))
>T : Symbol(T, Decl(nonNullableReduction.ts, 3, 16))
>value : Symbol(value, Decl(nonNullableReduction.ts, 3, 78))
>T : Symbol(T, Decl(nonNullableReduction.ts, 3, 16))

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
>test : Symbol(test, Decl(nonNullableReduction.ts, 3, 98))
>T : Symbol(T, Decl(nonNullableReduction.ts, 5, 14))
>f1 : Symbol(f1, Decl(nonNullableReduction.ts, 5, 17))
>Transform1 : Symbol(Transform1, Decl(nonNullableReduction.ts, 0, 0))
>T : Symbol(T, Decl(nonNullableReduction.ts, 5, 14))
>f2 : Symbol(f2, Decl(nonNullableReduction.ts, 5, 35))
>Transform2 : Symbol(Transform2, Decl(nonNullableReduction.ts, 2, 85))
>T : Symbol(T, Decl(nonNullableReduction.ts, 5, 14))

f1?.("hello");
>f1 : Symbol(f1, Decl(nonNullableReduction.ts, 5, 17))

f2?.("hello");
>f2 : Symbol(f2, Decl(nonNullableReduction.ts, 5, 35))
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
>f1 : Symbol(f1, Decl(nonNullableReduction.ts, 8, 1))
>T : Symbol(T, Decl(nonNullableReduction.ts, 10, 12))
>x : Symbol(x, Decl(nonNullableReduction.ts, 10, 15))
>T : Symbol(T, Decl(nonNullableReduction.ts, 10, 12))
>T : Symbol(T, Decl(nonNullableReduction.ts, 10, 12))

let z = x!; // NonNullable<T>
>z : Symbol(z, Decl(nonNullableReduction.ts, 11, 7))
>x : Symbol(x, Decl(nonNullableReduction.ts, 10, 15))
}

function f2<T, U extends null | undefined>(x: T | U) {
>f2 : Symbol(f2, Decl(nonNullableReduction.ts, 12, 1))
>T : Symbol(T, Decl(nonNullableReduction.ts, 14, 12))
>U : Symbol(U, Decl(nonNullableReduction.ts, 14, 14))
>x : Symbol(x, Decl(nonNullableReduction.ts, 14, 43))
>T : Symbol(T, Decl(nonNullableReduction.ts, 14, 12))
>U : Symbol(U, Decl(nonNullableReduction.ts, 14, 14))

let z = x!; // NonNullable<T>
>z : Symbol(z, Decl(nonNullableReduction.ts, 15, 7))
>x : Symbol(x, Decl(nonNullableReduction.ts, 14, 43))
}

50 changes: 50 additions & 0 deletions tests/baselines/reference/nonNullableReduction.types
@@ -0,0 +1,50 @@
=== tests/cases/compiler/nonNullableReduction.ts ===
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
>Transform1 : Transform1<T>
>value : string

type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;
>Transform2 : Transform2<T>
>value : string
>value : string

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
>test : <T>(f1: Transform1<T>, f2: Transform2<T>) => void
>f1 : Transform1<T>
>f2 : Transform2<T>

f1?.("hello");
>f1?.("hello") : T | undefined
>f1 : ((value: string) => T) | undefined
>"hello" : "hello"

f2?.("hello");
>f2?.("hello") : T | undefined
>f2 : ((value: string) => T) | ((value: string) => T) | undefined
>"hello" : "hello"
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
>f1 : <T>(x: T | (string extends T ? null | undefined : never)) => void
>x : T | (string extends T ? null | undefined : never)
>null : null

let z = x!; // NonNullable<T>
>z : NonNullable<T>
>x! : NonNullable<T>
>x : T | (string extends T ? null | undefined : never)
}

function f2<T, U extends null | undefined>(x: T | U) {
>f2 : <T, U extends null | undefined>(x: T | U) => void
>null : null
>x : T | U

let z = x!; // NonNullable<T>
>z : NonNullable<T>
>x! : NonNullable<T>
>x : T | U
}

32 changes: 32 additions & 0 deletions tests/baselines/reference/nonNullableReductionNonStrict.js
@@ -0,0 +1,32 @@
//// [nonNullableReductionNonStrict.ts]
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
f1?.("hello");
f2?.("hello");
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
let z = x!; // NonNullable<T>
}

function f2<T, U extends null | undefined>(x: T | U) {
let z = x!; // NonNullable<T>
}


//// [nonNullableReductionNonStrict.js]
// Repros from #43425
function test(f1, f2) {
f1 === null || f1 === void 0 ? void 0 : f1("hello");
f2 === null || f2 === void 0 ? void 0 : f2("hello");
}
function f1(x) {
var z = x; // NonNullable<T>
}
function f2(x) {
var z = x; // NonNullable<T>
}
61 changes: 61 additions & 0 deletions tests/baselines/reference/nonNullableReductionNonStrict.symbols
@@ -0,0 +1,61 @@
=== tests/cases/compiler/nonNullableReductionNonStrict.ts ===
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
>Transform1 : Symbol(Transform1, Decl(nonNullableReductionNonStrict.ts, 0, 0))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 2, 16))
>value : Symbol(value, Decl(nonNullableReductionNonStrict.ts, 2, 23))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 2, 16))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 2, 16))

type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;
>Transform2 : Symbol(Transform2, Decl(nonNullableReductionNonStrict.ts, 2, 85))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 3, 16))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 3, 16))
>value : Symbol(value, Decl(nonNullableReductionNonStrict.ts, 3, 42))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 3, 16))
>value : Symbol(value, Decl(nonNullableReductionNonStrict.ts, 3, 78))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 3, 16))

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
>test : Symbol(test, Decl(nonNullableReductionNonStrict.ts, 3, 98))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 5, 14))
>f1 : Symbol(f1, Decl(nonNullableReductionNonStrict.ts, 5, 17))
>Transform1 : Symbol(Transform1, Decl(nonNullableReductionNonStrict.ts, 0, 0))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 5, 14))
>f2 : Symbol(f2, Decl(nonNullableReductionNonStrict.ts, 5, 35))
>Transform2 : Symbol(Transform2, Decl(nonNullableReductionNonStrict.ts, 2, 85))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 5, 14))

f1?.("hello");
>f1 : Symbol(f1, Decl(nonNullableReductionNonStrict.ts, 5, 17))

f2?.("hello");
>f2 : Symbol(f2, Decl(nonNullableReductionNonStrict.ts, 5, 35))
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
>f1 : Symbol(f1, Decl(nonNullableReductionNonStrict.ts, 8, 1))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 10, 12))
>x : Symbol(x, Decl(nonNullableReductionNonStrict.ts, 10, 15))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 10, 12))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 10, 12))

let z = x!; // NonNullable<T>
>z : Symbol(z, Decl(nonNullableReductionNonStrict.ts, 11, 7))
>x : Symbol(x, Decl(nonNullableReductionNonStrict.ts, 10, 15))
}

function f2<T, U extends null | undefined>(x: T | U) {
>f2 : Symbol(f2, Decl(nonNullableReductionNonStrict.ts, 12, 1))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 14, 12))
>U : Symbol(U, Decl(nonNullableReductionNonStrict.ts, 14, 14))
>x : Symbol(x, Decl(nonNullableReductionNonStrict.ts, 14, 43))
>T : Symbol(T, Decl(nonNullableReductionNonStrict.ts, 14, 12))
>U : Symbol(U, Decl(nonNullableReductionNonStrict.ts, 14, 14))

let z = x!; // NonNullable<T>
>z : Symbol(z, Decl(nonNullableReductionNonStrict.ts, 15, 7))
>x : Symbol(x, Decl(nonNullableReductionNonStrict.ts, 14, 43))
}

50 changes: 50 additions & 0 deletions tests/baselines/reference/nonNullableReductionNonStrict.types
@@ -0,0 +1,50 @@
=== tests/cases/compiler/nonNullableReductionNonStrict.ts ===
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
>Transform1 : Transform1<T>
>value : string

type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;
>Transform2 : Transform2<T>
>value : string
>value : string

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
>test : <T>(f1: Transform1<T>, f2: Transform2<T>) => void
>f1 : Transform1<T>
>f2 : Transform2<T>

f1?.("hello");
>f1?.("hello") : T
>f1 : (value: string) => T
>"hello" : "hello"

f2?.("hello");
>f2?.("hello") : T
>f2 : ((value: string) => T) | ((value: string) => T)
>"hello" : "hello"
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
>f1 : <T>(x: T | (string extends T ? null | undefined : never)) => void
>x : T | (string extends T ? null : never)
>null : null

let z = x!; // NonNullable<T>
>z : T | (string extends T ? null : never)
>x! : T | (string extends T ? null : never)
>x : T | (string extends T ? null : never)
}

function f2<T, U extends null | undefined>(x: T | U) {
>f2 : <T, U extends null>(x: T | U) => void
>null : null
>x : T | U

let z = x!; // NonNullable<T>
>z : T | U
>x! : T | U
>x : T | U
}

19 changes: 19 additions & 0 deletions tests/cases/compiler/nonNullableReduction.ts
@@ -0,0 +1,19 @@
// @strict: true

// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
f1?.("hello");
f2?.("hello");
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
let z = x!; // NonNullable<T>
}

function f2<T, U extends null | undefined>(x: T | U) {
let z = x!; // NonNullable<T>
}
17 changes: 17 additions & 0 deletions tests/cases/compiler/nonNullableReductionNonStrict.ts
@@ -0,0 +1,17 @@
// Repros from #43425

type Transform1<T> = ((value: string) => T) | (string extends T ? undefined : never);
type Transform2<T> = string extends T ? ((value: string) => T) | undefined : (value: string) => T;

function test<T>(f1: Transform1<T>, f2: Transform2<T>) {
f1?.("hello");
f2?.("hello");
}

function f1<T>(x: T | (string extends T ? null | undefined : never)) {
let z = x!; // NonNullable<T>
}

function f2<T, U extends null | undefined>(x: T | U) {
let z = x!; // NonNullable<T>
}

0 comments on commit 3938958

Please sign in to comment.