Conversation

ahejlsberg

This PR introduces a number of changes affecting control flow analysis of truthy, equality, and typeof checks involving type unknown and unconstrained type variables in --strictNullChecks mode. Key to these changes is the fact that {}, the empty object type literal, is a supertype of all types except null and undefined. Thus, {} | null | undefined is effectively equivalent to unknown, and for an arbitrary type T, the intersection T & {} represents the non-nullable form of T.

The PR introduces the following new behaviors:

  • An unconstrained type parameter is no longer assignable to {}.
  • The predefined type NonNullable<T> is now an alias for T & {}.
  • An intersection T & {}, where T is non-generic and not null or undefined, reduces to just T (null & {} and undefined & {} already reduce to never). See below for exceptions for string & {}, number & {}, and bigint & {}.
  • An intersection undefined & void reduces to just undefined.
  • Any type is assignable to a union type that contains {}, null, and undefined.
  • In control flow analysis, unknown behaves similarly to the union type {} | null | undefined.
  • In control flow analysis of truthy checks, generic types are intersected with {} in the true branch.
  • In control flow analysis of equality comparisons with null or undefined, generic types are intersected with {}, {} | null, or {} | undefined in the false branch.
  • In control flow analysis of typeof x === "object" expressions, generic types are intersected with object and/or null in the true branch (typeof checks for other types already produce intersections in a similar manner).

Some examples:

type T1 = {} & string;  // string
type T2 = {} & 'a';  // 'a'
type T3 = {} & object;  // object
type T4 = {} & { x: number };  // { x: number }
type T5 = {} & null;  // never
type T6 = {} & undefined;  // never
type T7 = undefined & void;  // undefined

function f1(u: unknown) {
    let x1: {} = u;  // Error
    let x2: {} | null | undefined = u;  // Ok
    let x3: {} | { x: string } | null | undefined = u;  // Ok
}

function f2(x: unknown) {
    if (x) {
        x;  // {}
    }
    else {
        x;  // unknown
    }
}

function f3<T>(x: T) {
    if (x) {
        x;  // T & {}
    }
    else {
        x;  // T
    }
}

function f4(x: unknown) {
    if (x !== undefined) {
        x;  // {} | null
    }
    else {
        x;  // undefined
    }
    if (x !== null) {
        x;  // {} | undefined
    }
    else {
        x;  // null
    }
    if (x !== undefined && x !== null) {
        x;  // {}
    }
    else {
        x;  // null | undefined
    }
    if (x != undefined) {
        x;  // {}
    }
    else {
        x;  // null | undefined
    }
    if (x != null) {
        x;  // {}
    }
    else {
        x;  // null | undefined
    }
}

function f5<T>(x: T) {
    if (x !== undefined) {
        x;  // T & ({} | null)
    }
    else {
        x;  // T
    }
    if (x !== null) {
        x;  // T & ({} | undefined)
    }
    else {
        x;  // T
    }
    if (x !== undefined && x !== null) {
        x;  // {}
    }
    else {
        x;  // T
    }
    if (x != undefined) {
        x;  // {}
    }
    else {
        x;  // T
    }
    if (x != null) {
        x;  // {}
    }
    else {
        x;  // T
    }
}

function f6<T>(x: T) {
    if (typeof x === "object") {
        x;  // T & object | T & null
    }
    if (x && typeof x === "object") {
        x;  // T & object
    }
    if (typeof x === "object" && x) {
        x;  // T & object
    }
}

function ensureNotNull<T>(x: T) {
    if (x === null) throw Error();
    return x;  // T & ({} | undefined)
}

function ensureNotUndefined<T>(x: T) {
    if (x === undefined) throw Error();
    return x;  // T & ({} | null)
}

function ensureNotNullOrUndefined<T>(x: T) {
    return ensureNotUndefined(ensureNotNull(x));  // T & {}
}

function f7(a: string | undefined, b: number | null | undefined) {
    let a1 = ensureNotNullOrUndefined(a);  // string
    let b1 = ensureNotNullOrUndefined(b);  // number
}

Note the manner in which types are properly inferred, combined, and reduced in the ensureNotXXX functions. This contrasts with the NonNullable<T> conditional type provided in lib.d.ts, which unfortunately combines and reduces poorly. For example, NonNullable<NonNullable<T>> doesn't inherently reduce to NonNullable<T>, sometimes leading to needlessly complex types. For this and other reasons we intend to investigate switching NonNullable<T> to be an alias for T & {}.

For backwards compatibility, special exceptions to the T & {} type reduction rules existing for intersections written explicitly as string & {}, number & {}, and bigint & {} (as opposed to created through instantiation of a generic type T & {}). These types are used in a few frameworks (e.g. react and csstype) to construct types that permit any string, number, or bigint, but has statement completion hints for common literal values. For example:

type Alignment = string & {} | "left" | "center" | "right";

The special string & {} type prevents subtype reduction from taking place in the union type, thus preserving the literal types, but otherwise any string value is assignable to the type.

This PR reinstatates #48366 (which was removed from 4.7 due to concerns over breaking changes).

Fixes #23368.
Fixes #31908.
Fixes #32347.
Fixes #43997.
Fixes #44446.
Fixes #48048.
Fixes #48468.
Fixes #48691.
Fixes #49005.
Fixes #49191.

@typescript-bottypescript-bot added Author: Team For Uncommitted BugPR for untriaged, rejected, closed or missing buglabels May 15, 2022
@ahejlsberg

@typescript-bot test this
@typescript-bot user test this inline
@typescript-bot run dt
@typescript-bot perf test faster

@typescript-bot

Heya @ahejlsberg, I've started to run the abridged perf test suite on this PR at 39326d7. You can monitor the build here.

Update: The results are in!

@typescript-bot

Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at 39326d7. You can monitor the build here.

@typescript-bot

Heya @ahejlsberg, I've started to run the extended test suite on this PR at 39326d7. You can monitor the build here.

@typescript-bot

Heya @ahejlsberg, I've started to run the diff-based community code test suite on this PR at 39326d7. You can monitor the build here.

Update: The results are in!

@typescript-bot

@ahejlsberg
The results of the user tests run you requested are in!

Here they are:

Comparison Report - main..refs/pull/49119/merge

[async]

1 of 1 projects failed to build with the old tsc

/mnt/ts_downloads/async/tsconfig.json

  • error TS2339: Property 'iterator' does not exist on type 'never'.
    • /mnt/ts_downloads/async/node_modules/async/dist/async.js(477,61)
    • /mnt/ts_downloads/async/node_modules/async/internal/getIterator.js(11,61)

@typescript-bot

@ahejlsberg
The results of the perf run you requested are in!

Here they are:

Comparison Report - main..49119

Metricmain49119DeltaBestWorst
Angular - node (v14.15.1, x64)
Memory used333,610k (± 0.01%)333,600k (± 0.01%)-9k (- 0.00%)333,548k333,664k
Parse Time2.04s (± 0.44%)2.07s (± 0.96%)+0.02s (+ 1.08%)2.04s2.13s
Bind Time0.88s (± 0.95%)0.88s (± 0.78%)0.00s ( 0.00%)0.86s0.89s
Check Time5.65s (± 0.55%)5.69s (± 0.54%)+0.03s (+ 0.58%)5.61s5.76s
Emit Time6.29s (± 0.74%)6.37s (± 0.93%)+0.08s (+ 1.34%)6.19s6.49s
Total Time14.86s (± 0.49%)15.00s (± 0.62%)+0.14s (+ 0.94%)14.70s15.15s
Compiler-Unions - node (v14.15.1, x64)
Memory used192,272k (± 0.01%)192,173k (± 0.12%)-99k (- 0.05%)191,218k192,363k
Parse Time0.85s (± 0.87%)0.85s (± 0.35%)-0.00s (- 0.24%)0.84s0.85s
Bind Time0.56s (± 0.84%)0.56s (± 0.84%)+0.00s (+ 0.54%)0.55s0.57s
Check Time7.55s (± 0.47%)7.61s (± 0.68%)+0.06s (+ 0.78%)7.52s7.75s
Emit Time2.50s (± 0.66%)2.51s (± 0.89%)+0.01s (+ 0.28%)2.47s2.58s
Total Time11.46s (± 0.28%)11.53s (± 0.54%)+0.06s (+ 0.56%)11.43s11.74s
Monaco - node (v14.15.1, x64)
Memory used325,609k (± 0.00%)325,639k (± 0.00%)+30k (+ 0.01%)325,603k325,665k
Parse Time1.56s (± 0.40%)1.57s (± 0.85%)+0.02s (+ 1.03%)1.55s1.61s
Bind Time0.78s (± 1.15%)0.78s (± 0.71%)+0.00s (+ 0.26%)0.77s0.79s
Check Time5.54s (± 0.51%)5.54s (± 0.36%)+0.01s (+ 0.13%)5.51s5.58s
Emit Time3.31s (± 0.47%)3.33s (± 0.75%)+0.02s (+ 0.51%)3.27s3.37s
Total Time11.18s (± 0.26%)11.22s (± 0.24%)+0.05s (+ 0.42%)11.15s11.26s
TFS - node (v14.15.1, x64)
Memory used289,123k (± 0.01%)289,109k (± 0.01%)-14k (- 0.00%)289,052k289,157k
Parse Time1.36s (± 1.72%)1.37s (± 0.73%)+0.01s (+ 0.51%)1.35s1.40s
Bind Time0.72s (± 0.31%)0.72s (± 0.55%)+0.00s (+ 0.00%)0.71s0.73s
Check Time5.21s (± 0.57%)5.21s (± 0.44%)+0.00s (+ 0.08%)5.17s5.26s
Emit Time3.54s (± 1.90%)3.60s (± 1.11%)+0.06s (+ 1.70%)3.44s3.64s
Total Time10.83s (± 0.73%)10.90s (± 0.41%)+0.07s (+ 0.65%)10.77s11.01s
material-ui - node (v14.15.1, x64)
Memory used445,620k (± 0.06%)445,758k (± 0.00%)+138k (+ 0.03%)445,724k445,788k
Parse Time1.88s (± 0.59%)1.88s (± 0.59%)0.00s ( 0.00%)1.86s1.91s
Bind Time0.70s (± 0.68%)0.70s (± 0.68%)0.00s ( 0.00%)0.69s0.71s
Check Time13.12s (± 0.84%)13.15s (± 0.63%)+0.03s (+ 0.23%)12.95s13.31s
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)0.00s ( NaN%)0.00s0.00s
Total Time15.70s (± 0.73%)15.73s (± 0.54%)+0.03s (+ 0.21%)15.52s15.91s
xstate - node (v14.15.1, x64)
Memory used535,261k (± 0.00%)535,348k (± 0.00%)+87k (+ 0.02%)535,298k535,383k
Parse Time2.58s (± 0.44%)2.60s (± 0.57%)+0.02s (+ 0.77%)2.57s2.64s
Bind Time1.15s (± 0.65%)1.16s (± 1.25%)+0.01s (+ 1.13%)1.14s1.20s
Check Time1.52s (± 0.48%)1.52s (± 0.45%)+0.00s (+ 0.20%)1.51s1.54s
Emit Time0.07s (± 0.00%)0.07s (± 0.00%)0.00s ( 0.00%)0.07s0.07s
Total Time5.32s (± 0.20%)5.36s (± 0.40%)+0.04s (+ 0.75%)5.32s5.42s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-210-generic
Architecturex64
Available Memory16 GB
Available Memory1 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v14.15.1, x64)
Scenarios
  • Angular - node (v14.15.1, x64)
  • Compiler-Unions - node (v14.15.1, x64)
  • Monaco - node (v14.15.1, x64)
  • TFS - node (v14.15.1, x64)
  • material-ui - node (v14.15.1, x64)
  • xstate - node (v14.15.1, x64)
BenchmarkNameIterations
Current4911910
Baselinemain10

Developer Information:

Download Benchmark

@ahejlsberg

@typescript-bot test this
@typescript-bot user test this inline
@typescript-bot run dt
@typescript-bot perf test faster

@typescript-bot

Heya @ahejlsberg, I've started to run the abridged perf test suite on this PR at 986963c. You can monitor the build here.

Update: The results are in!

@typescript-bot

Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at 986963c. You can monitor the build here.

@typescript-bot

Heya @ahejlsberg, I've started to run the diff-based community code test suite on this PR at 986963c. You can monitor the build here.

@typescript-bot

Heya @ahejlsberg, I've started to run the extended test suite on this PR at 986963c. You can monitor the build here.

@typescript-bot

@ahejlsberg
The results of the perf run you requested are in!

Here they are:

Comparison Report - main..49119

Metricmain49119DeltaBestWorst
Angular - node (v14.15.1, x64)
Memory used333,610k (± 0.01%)333,609k (± 0.00%)-1k (- 0.00%)333,591k333,642k
Parse Time2.04s (± 0.44%)2.04s (± 0.46%)-0.00s (- 0.10%)2.02s2.06s
Bind Time0.88s (± 0.95%)0.87s (± 0.46%)-0.00s (- 0.46%)0.86s0.88s
Check Time5.65s (± 0.55%)5.65s (± 0.39%)-0.00s (- 0.02%)5.60s5.69s
Emit Time6.29s (± 0.74%)6.35s (± 0.88%)+0.06s (+ 0.97%)6.27s6.49s
Total Time14.86s (± 0.49%)14.91s (± 0.44%)+0.05s (+ 0.35%)14.78s15.04s
Compiler-Unions - node (v14.15.1, x64)
Memory used192,272k (± 0.01%)192,279k (± 0.02%)+7k (+ 0.00%)192,180k192,344k
Parse Time0.85s (± 0.87%)0.85s (± 0.73%)-0.00s (- 0.12%)0.84s0.86s
Bind Time0.56s (± 0.84%)0.56s (± 0.65%)+0.00s (+ 0.71%)0.56s0.57s
Check Time7.55s (± 0.47%)7.55s (± 0.40%)-0.00s (- 0.03%)7.50s7.63s
Emit Time2.50s (± 0.66%)2.50s (± 0.75%)+0.00s (+ 0.12%)2.46s2.54s
Total Time11.46s (± 0.28%)11.46s (± 0.35%)+0.00s (+ 0.02%)11.39s11.59s
Monaco - node (v14.15.1, x64)
Memory used325,609k (± 0.00%)325,627k (± 0.01%)+18k (+ 0.01%)325,586k325,688k
Parse Time1.56s (± 0.40%)1.57s (± 0.67%)+0.01s (+ 0.90%)1.56s1.60s
Bind Time0.78s (± 1.15%)0.77s (± 0.72%)-0.00s (- 0.52%)0.76s0.78s
Check Time5.54s (± 0.51%)5.59s (± 0.32%)+0.06s (+ 0.99%)5.54s5.63s
Emit Time3.31s (± 0.47%)3.35s (± 0.75%)+0.04s (+ 1.18%)3.30s3.40s
Total Time11.18s (± 0.26%)11.29s (± 0.37%)+0.11s (+ 0.98%)11.20s11.36s
TFS - node (v14.15.1, x64)
Memory used289,123k (± 0.01%)289,132k (± 0.00%)+8k (+ 0.00%)289,103k289,147k
Parse Time1.36s (± 1.72%)1.36s (± 1.10%)-0.01s (- 0.44%)1.31s1.38s
Bind Time0.72s (± 0.31%)0.72s (± 0.50%)+0.00s (+ 0.42%)0.72s0.73s
Check Time5.21s (± 0.57%)5.21s (± 0.29%)+0.00s (+ 0.02%)5.18s5.24s
Emit Time3.54s (± 1.90%)3.56s (± 1.93%)+0.03s (+ 0.76%)3.41s3.65s
Total Time10.83s (± 0.73%)10.85s (± 0.69%)+0.02s (+ 0.22%)10.67s10.97s
material-ui - node (v14.15.1, x64)
Memory used445,620k (± 0.06%)445,722k (± 0.01%)+102k (+ 0.02%)445,618k445,786k
Parse Time1.88s (± 0.59%)1.88s (± 0.56%)-0.01s (- 0.37%)1.86s1.91s
Bind Time0.70s (± 0.68%)0.70s (± 0.74%)+0.00s (+ 0.57%)0.69s0.71s
Check Time13.12s (± 0.84%)13.11s (± 0.61%)-0.01s (- 0.08%)12.96s13.36s
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)0.00s ( NaN%)0.00s0.00s
Total Time15.70s (± 0.73%)15.69s (± 0.53%)-0.01s (- 0.07%)15.52s15.95s
xstate - node (v14.15.1, x64)
Memory used535,261k (± 0.00%)535,395k (± 0.00%)+134k (+ 0.03%)535,367k535,423k
Parse Time2.58s (± 0.44%)2.60s (± 0.58%)+0.02s (+ 0.77%)2.57s2.63s
Bind Time1.15s (± 0.65%)1.15s (± 1.04%)+0.00s (+ 0.26%)1.13s1.19s
Check Time1.52s (± 0.48%)1.52s (± 0.70%)+0.00s (+ 0.20%)1.51s1.55s
Emit Time0.07s (± 0.00%)0.07s (± 0.00%)0.00s ( 0.00%)0.07s0.07s
Total Time5.32s (± 0.20%)5.35s (± 0.42%)+0.03s (+ 0.53%)5.30s5.39s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-210-generic
Architecturex64
Available Memory16 GB
Available Memory1 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v14.15.1, x64)
Scenarios
  • Angular - node (v14.15.1, x64)
  • Compiler-Unions - node (v14.15.1, x64)
  • Monaco - node (v14.15.1, x64)
  • TFS - node (v14.15.1, x64)
  • material-ui - node (v14.15.1, x64)
  • xstate - node (v14.15.1, x64)
BenchmarkNameIterations
Current4911910
Baselinemain10

Developer Information:

Download Benchmark

@ahejlsberg

@user test this inline

@jcalz

Is there a reason why this narrowing wasn't implemented for typeof x === "undefined" the way it was for x === undefined? See this SO question

@jakebailey

I feel like that's an oversight and probably deserves its own issue. (But, I'm not Anders 😄)

@Andarist

@jcalz what you reported here, in your last comment, was just fixed 2 days ago in #52456

@uid11

It seems that in the examples in the main message in the function f5<T> the type parameter T is missing in a couple of places:

function f5<T>(x: T) {
    ...
    if (x !== undefined && x !== null) {
        x;  // {} -- should be T & {}
    }
    else {
        x;  // T
    }
    if (x != undefined) {
        x;  // {} -- should be NonNullable<T>
    }
    else {
        x;  // T
    }
    if (x != null) {
        x;  // {} -- should be NonNullable<T>
    }
    else {
        x;  // T
    }
}

But this is clear from the context and will not confuse anyone too much, I think.

@jakebaileyjakebailey mentioned this pull request Mar 15, 2024
Sign up for free to join this conversation on . Already have an account? Sign in to comment