Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment