Skip to content

Improve intersection reduction and CFA for truthy, equality, and typeof checks #49119

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 32 commits into from
May 27, 2022

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented May 15, 2022

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-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels May 15, 2022
@ahejlsberg
Copy link
Member Author

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented May 15, 2022

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
Copy link
Collaborator

typescript-bot commented May 15, 2022

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
Copy link
Collaborator

typescript-bot commented May 15, 2022

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented May 15, 2022

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
Copy link
Collaborator

@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
Copy link
Collaborator

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

Here they are:

Comparison Report - main..49119

Metric main 49119 Delta Best Worst
Angular - node (v14.15.1, x64)
Memory used 333,610k (± 0.01%) 333,600k (± 0.01%) -9k (- 0.00%) 333,548k 333,664k
Parse Time 2.04s (± 0.44%) 2.07s (± 0.96%) +0.02s (+ 1.08%) 2.04s 2.13s
Bind Time 0.88s (± 0.95%) 0.88s (± 0.78%) 0.00s ( 0.00%) 0.86s 0.89s
Check Time 5.65s (± 0.55%) 5.69s (± 0.54%) +0.03s (+ 0.58%) 5.61s 5.76s
Emit Time 6.29s (± 0.74%) 6.37s (± 0.93%) +0.08s (+ 1.34%) 6.19s 6.49s
Total Time 14.86s (± 0.49%) 15.00s (± 0.62%) +0.14s (+ 0.94%) 14.70s 15.15s
Compiler-Unions - node (v14.15.1, x64)
Memory used 192,272k (± 0.01%) 192,173k (± 0.12%) -99k (- 0.05%) 191,218k 192,363k
Parse Time 0.85s (± 0.87%) 0.85s (± 0.35%) -0.00s (- 0.24%) 0.84s 0.85s
Bind Time 0.56s (± 0.84%) 0.56s (± 0.84%) +0.00s (+ 0.54%) 0.55s 0.57s
Check Time 7.55s (± 0.47%) 7.61s (± 0.68%) +0.06s (+ 0.78%) 7.52s 7.75s
Emit Time 2.50s (± 0.66%) 2.51s (± 0.89%) +0.01s (+ 0.28%) 2.47s 2.58s
Total Time 11.46s (± 0.28%) 11.53s (± 0.54%) +0.06s (+ 0.56%) 11.43s 11.74s
Monaco - node (v14.15.1, x64)
Memory used 325,609k (± 0.00%) 325,639k (± 0.00%) +30k (+ 0.01%) 325,603k 325,665k
Parse Time 1.56s (± 0.40%) 1.57s (± 0.85%) +0.02s (+ 1.03%) 1.55s 1.61s
Bind Time 0.78s (± 1.15%) 0.78s (± 0.71%) +0.00s (+ 0.26%) 0.77s 0.79s
Check Time 5.54s (± 0.51%) 5.54s (± 0.36%) +0.01s (+ 0.13%) 5.51s 5.58s
Emit Time 3.31s (± 0.47%) 3.33s (± 0.75%) +0.02s (+ 0.51%) 3.27s 3.37s
Total Time 11.18s (± 0.26%) 11.22s (± 0.24%) +0.05s (+ 0.42%) 11.15s 11.26s
TFS - node (v14.15.1, x64)
Memory used 289,123k (± 0.01%) 289,109k (± 0.01%) -14k (- 0.00%) 289,052k 289,157k
Parse Time 1.36s (± 1.72%) 1.37s (± 0.73%) +0.01s (+ 0.51%) 1.35s 1.40s
Bind Time 0.72s (± 0.31%) 0.72s (± 0.55%) +0.00s (+ 0.00%) 0.71s 0.73s
Check Time 5.21s (± 0.57%) 5.21s (± 0.44%) +0.00s (+ 0.08%) 5.17s 5.26s
Emit Time 3.54s (± 1.90%) 3.60s (± 1.11%) +0.06s (+ 1.70%) 3.44s 3.64s
Total Time 10.83s (± 0.73%) 10.90s (± 0.41%) +0.07s (+ 0.65%) 10.77s 11.01s
material-ui - node (v14.15.1, x64)
Memory used 445,620k (± 0.06%) 445,758k (± 0.00%) +138k (+ 0.03%) 445,724k 445,788k
Parse Time 1.88s (± 0.59%) 1.88s (± 0.59%) 0.00s ( 0.00%) 1.86s 1.91s
Bind Time 0.70s (± 0.68%) 0.70s (± 0.68%) 0.00s ( 0.00%) 0.69s 0.71s
Check Time 13.12s (± 0.84%) 13.15s (± 0.63%) +0.03s (+ 0.23%) 12.95s 13.31s
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) 0.00s ( NaN%) 0.00s 0.00s
Total Time 15.70s (± 0.73%) 15.73s (± 0.54%) +0.03s (+ 0.21%) 15.52s 15.91s
xstate - node (v14.15.1, x64)
Memory used 535,261k (± 0.00%) 535,348k (± 0.00%) +87k (+ 0.02%) 535,298k 535,383k
Parse Time 2.58s (± 0.44%) 2.60s (± 0.57%) +0.02s (+ 0.77%) 2.57s 2.64s
Bind Time 1.15s (± 0.65%) 1.16s (± 1.25%) +0.01s (+ 1.13%) 1.14s 1.20s
Check Time 1.52s (± 0.48%) 1.52s (± 0.45%) +0.00s (+ 0.20%) 1.51s 1.54s
Emit Time 0.07s (± 0.00%) 0.07s (± 0.00%) 0.00s ( 0.00%) 0.07s 0.07s
Total Time 5.32s (± 0.20%) 5.36s (± 0.40%) +0.04s (+ 0.75%) 5.32s 5.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)
Benchmark Name Iterations
Current 49119 10
Baseline main 10

Developer Information:

Download Benchmark

@ahejlsberg
Copy link
Member Author

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented May 15, 2022

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
Copy link
Collaborator

typescript-bot commented May 15, 2022

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
Copy link
Collaborator

typescript-bot commented May 15, 2022

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
Copy link
Collaborator

typescript-bot commented May 15, 2022

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

@typescript-bot
Copy link
Collaborator

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

Here they are:

Comparison Report - main..49119

Metric main 49119 Delta Best Worst
Angular - node (v14.15.1, x64)
Memory used 333,610k (± 0.01%) 333,609k (± 0.00%) -1k (- 0.00%) 333,591k 333,642k
Parse Time 2.04s (± 0.44%) 2.04s (± 0.46%) -0.00s (- 0.10%) 2.02s 2.06s
Bind Time 0.88s (± 0.95%) 0.87s (± 0.46%) -0.00s (- 0.46%) 0.86s 0.88s
Check Time 5.65s (± 0.55%) 5.65s (± 0.39%) -0.00s (- 0.02%) 5.60s 5.69s
Emit Time 6.29s (± 0.74%) 6.35s (± 0.88%) +0.06s (+ 0.97%) 6.27s 6.49s
Total Time 14.86s (± 0.49%) 14.91s (± 0.44%) +0.05s (+ 0.35%) 14.78s 15.04s
Compiler-Unions - node (v14.15.1, x64)
Memory used 192,272k (± 0.01%) 192,279k (± 0.02%) +7k (+ 0.00%) 192,180k 192,344k
Parse Time 0.85s (± 0.87%) 0.85s (± 0.73%) -0.00s (- 0.12%) 0.84s 0.86s
Bind Time 0.56s (± 0.84%) 0.56s (± 0.65%) +0.00s (+ 0.71%) 0.56s 0.57s
Check Time 7.55s (± 0.47%) 7.55s (± 0.40%) -0.00s (- 0.03%) 7.50s 7.63s
Emit Time 2.50s (± 0.66%) 2.50s (± 0.75%) +0.00s (+ 0.12%) 2.46s 2.54s
Total Time 11.46s (± 0.28%) 11.46s (± 0.35%) +0.00s (+ 0.02%) 11.39s 11.59s
Monaco - node (v14.15.1, x64)
Memory used 325,609k (± 0.00%) 325,627k (± 0.01%) +18k (+ 0.01%) 325,586k 325,688k
Parse Time 1.56s (± 0.40%) 1.57s (± 0.67%) +0.01s (+ 0.90%) 1.56s 1.60s
Bind Time 0.78s (± 1.15%) 0.77s (± 0.72%) -0.00s (- 0.52%) 0.76s 0.78s
Check Time 5.54s (± 0.51%) 5.59s (± 0.32%) +0.06s (+ 0.99%) 5.54s 5.63s
Emit Time 3.31s (± 0.47%) 3.35s (± 0.75%) +0.04s (+ 1.18%) 3.30s 3.40s
Total Time 11.18s (± 0.26%) 11.29s (± 0.37%) +0.11s (+ 0.98%) 11.20s 11.36s
TFS - node (v14.15.1, x64)
Memory used 289,123k (± 0.01%) 289,132k (± 0.00%) +8k (+ 0.00%) 289,103k 289,147k
Parse Time 1.36s (± 1.72%) 1.36s (± 1.10%) -0.01s (- 0.44%) 1.31s 1.38s
Bind Time 0.72s (± 0.31%) 0.72s (± 0.50%) +0.00s (+ 0.42%) 0.72s 0.73s
Check Time 5.21s (± 0.57%) 5.21s (± 0.29%) +0.00s (+ 0.02%) 5.18s 5.24s
Emit Time 3.54s (± 1.90%) 3.56s (± 1.93%) +0.03s (+ 0.76%) 3.41s 3.65s
Total Time 10.83s (± 0.73%) 10.85s (± 0.69%) +0.02s (+ 0.22%) 10.67s 10.97s
material-ui - node (v14.15.1, x64)
Memory used 445,620k (± 0.06%) 445,722k (± 0.01%) +102k (+ 0.02%) 445,618k 445,786k
Parse Time 1.88s (± 0.59%) 1.88s (± 0.56%) -0.01s (- 0.37%) 1.86s 1.91s
Bind Time 0.70s (± 0.68%) 0.70s (± 0.74%) +0.00s (+ 0.57%) 0.69s 0.71s
Check Time 13.12s (± 0.84%) 13.11s (± 0.61%) -0.01s (- 0.08%) 12.96s 13.36s
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) 0.00s ( NaN%) 0.00s 0.00s
Total Time 15.70s (± 0.73%) 15.69s (± 0.53%) -0.01s (- 0.07%) 15.52s 15.95s
xstate - node (v14.15.1, x64)
Memory used 535,261k (± 0.00%) 535,395k (± 0.00%) +134k (+ 0.03%) 535,367k 535,423k
Parse Time 2.58s (± 0.44%) 2.60s (± 0.58%) +0.02s (+ 0.77%) 2.57s 2.63s
Bind Time 1.15s (± 0.65%) 1.15s (± 1.04%) +0.00s (+ 0.26%) 1.13s 1.19s
Check Time 1.52s (± 0.48%) 1.52s (± 0.70%) +0.00s (+ 0.20%) 1.51s 1.55s
Emit Time 0.07s (± 0.00%) 0.07s (± 0.00%) 0.00s ( 0.00%) 0.07s 0.07s
Total Time 5.32s (± 0.20%) 5.35s (± 0.42%) +0.03s (+ 0.53%) 5.30s 5.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)
Benchmark Name Iterations
Current 49119 10
Baseline main 10

Developer Information:

Download Benchmark

@ahejlsberg
Copy link
Member Author

@user test this inline

@jcalz
Copy link
Contributor

jcalz commented Sep 17, 2022

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
Copy link
Member

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

@Andarist
Copy link
Contributor

Andarist commented Feb 1, 2023

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

@uid11
Copy link

uid11 commented Sep 28, 2023

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.

# for free to join this conversation on GitHub. Already have an account? # to comment