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

isArray() preserve mutability and element type #48228

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

jablko
Copy link
Contributor

@jablko jablko commented Mar 11, 2022

Reopening #42316, with any and unknown backward compatibility: #42316 (comment)

There are a couple of related issues here:

  1. Array.isArray() narrows readonly T[] -> mutable any[]
  2. Consequently the false branch doesn't remove readonly T[]
  3. It loses the element or iterated type of wider types ArrayLike<T> and Iterable<T>

#17002 concerns issues 1 and 2. 1 and 3 are pretty similar --- #33700 calls out issue 3, the still-wider type Iterable<T> vs. readonly T[].

#42316 fixed #17002 (1 and 2) but not #33700 (issue 3), by changing the return type from arg is any[] -> arg is readonly unknown[]. That preserves mutability and therefore removes it from the false branch. It narrows:

  • T[] -> T[] & readonly unknown[] (which is mutable because the T[] component is mutable)
  • readonly T[] -> readonly T[] & readonly unknown[] (immutable)

It also preserves the element type of readonly T[].

#42316 was closed because it narrowed any and unknown -> readonly unknown[] vs. any[] (which is the current behavior), breaking code that relies on unknown narrowing to mutable, or narrowing to elements of type any:

declare const u: unknown;
if (Array.isArray(u)) {
  u[1] = 12;
  u[1].length;
}

The current behavior is exceedingly permissive in those cases. The change added errors that weren't there before, but aren't necessarily correct or incorrect, because any and unknown can be anything. The fix wasn't worth the breakage: #42316 (comment)

🩹 Proposed solution

This PR addresses backward compatibility, and also fixes #33700 (issue 3), by instead adding overloads for:

    isArray<T>(arg: ArrayLike<T>): arg is readonly T[];
    isArray<T>(arg: Iterable<T>): arg is readonly T[];

This fixes 1, 2, and 3, for ArrayLike<T>, Iterable<T>, and their narrower types, including readonly T[], while preserving the current behavior for everything else, including any and unknown.

Fixes #17002
Fixes #33700
Fixes #41808

⏯️ Playground link

Workbench repro.

🧑‍💻 Code

// @target: es2015
// @errors: 2322 4104

//interface ArrayConstructor {
//  isArray<T>(arg: ArrayLike<T>): arg is readonly T[];
//  isArray<T>(arg: Iterable<T>): arg is readonly T[];
//}

// https://github.com/microsoft/TypeScript/issues/17002
// Preserves mutability, false branch removes arrays, mutable or not

declare const mutable: string | string[];
if (Array.isArray(mutable)) {
  const stillMutable: string[] = mutable;
} else {
  const narrowed: string = mutable;
}

declare const immutable: string | readonly string[];
if (Array.isArray(immutable)) {
  const notMutable: string[] = immutable; // Should fail: readonly string[] isn't assignable to string[]
} else {
  const narrowed: string = immutable;
}

// https://github.com/microsoft/TypeScript/issues/33700
// Preserves element or iterated type of wider types

declare const arrayLike: string | ArrayLike<string>;
if (Array.isArray(arrayLike)) {
  const arrayOfElementType: readonly string[] = arrayLike;
  const notArrayOfAny: readonly void[] = arrayLike; // Should fail: string isn't assignable to void
}

declare const iterable: string | Iterable<string>;
if (Array.isArray(iterable)) {
  const arrayOfIteratedType: readonly string[] = iterable;
  const notArrayOfAny: readonly void[] = iterable; // Should fail: string isn't assignable to void
}

// https://github.com/microsoft/TypeScript/pull/42316#discussion_r823218462
// any and unknown backward compatibility

declare const any: any;
if (Array.isArray(any)) {
  const mutableArrayOfAny: void[] = any;
  const notAny: void = any; // Should fail: any[] isn't assignable to void
}

declare const unknown: unknown;
if (Array.isArray(unknown)) {
  const mutableArrayOfAny: void[] = unknown;
  const notAny: void = unknown; // Should fail: any[] isn't assignable to void
}

🙁 Actual behavior

// https://github.com/microsoft/TypeScript/issues/17002
// Preserves mutability, false branch removes arrays, mutable or not

declare const immutable: string | readonly string[];
if (Array.isArray(immutable)) {
  const notMutable: string[] = immutable; // ❌ No error: You're free to mutate immutable
} else {
  const narrowed: string = immutable; // ❌ False branch removes mutable arrays only
}

// https://github.com/microsoft/TypeScript/issues/33700
// Preserves element or iterated type of wider types

declare const iterable: string | Iterable<string>;
if (Array.isArray(iterable)) {
  const arrayOfIteratedType: readonly string[] = iterable;
  const notArrayOfAny: readonly void[] = iterable; // ❌ No Error: Iterable<string> -> any[]
}
$ tsc --target es2015 input.ts 
input.ts:8:9 - error TS2322: Type 'string | readonly string[]' is not assignable to type 'string'.
  Type 'readonly string[]' is not assignable to type 'string'.

8   const narrowed: string = immutable; // ❌ False branch removes mutable arrays only
          ~~~~~~~~


Found 1 error.

🙂 Expected behavior

With this PR:

// https://github.com/microsoft/TypeScript/issues/17002
// Preserves mutability, false branch removes arrays, mutable or not

declare const immutable: string | readonly string[];
if (Array.isArray(immutable)) {
  const notMutable: string[] = immutable; // ✔️ readonly string[] isn't assignable to string[]
} else {
  const narrowed: string = immutable; // ✔️ False branch removes arrays, mutable or not
}

// https://github.com/microsoft/TypeScript/issues/33700
// Preserves element or iterated type of wider types

declare const iterable: string | Iterable<string>;
if (Array.isArray(iterable)) {
  const arrayOfIteratedType: readonly string[] = iterable;
  const notArrayOfAny: readonly void[] = iterable; // ✔️ string isn't assignable to void
}
$ node built/local/tsc.js --target es2015 input.ts 
input.ts:6:9 - error TS4104: The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

6   const notMutable: string[] = immutable; // ✔️ readonly string[] isn't assignable to string[]
          ~~~~~~~~~~

input.ts:17:9 - error TS2322: Type 'readonly string[]' is not assignable to type 'readonly void[]'.
  Type 'string' is not assignable to type 'void'.

17   const notArrayOfAny: readonly void[] = iterable; // ✔️ string isn't assignable to void
           ~~~~~~~~~~~~~


Found 2 errors in the same file, starting at: input.ts:6

@typescript-bot typescript-bot added the For Milestone Bug PRs that fix a bug with a specific milestone label Mar 11, 2022
@typescript-bot
Copy link
Collaborator

The TypeScript team hasn't accepted the linked issue #33700. If you can get it accepted, this PR will have a better chance of being reviewed.

1 similar comment
@typescript-bot
Copy link
Collaborator

The TypeScript team hasn't accepted the linked issue #33700. If you can get it accepted, this PR will have a better chance of being reviewed.

@RyanCavanaugh
Copy link
Member

@DanielRosenwasser let's discuss this next week - seems promising

@SPGoding
Copy link

SPGoding commented Jun 2, 2022

Not sure if I get this right, but the immutable case only seems to work because both string and readonly string[] are Iterable<string>. If you change string to number instead the type checker reverts back to the old behavior:

// https://github.com/microsoft/TypeScript/issues/17002

declare const immutable: number | readonly number[];
if (Array.isArray(immutable)) {
  const notMutable: number[] = immutable; // ❌ No error: type of immutable is `any[]`
} else {
  const narrowed: number = immutable; // ❌ Error: `readonly number[]` is not removed from false branch.
}

@jakebailey
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 25, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top400 ✅ Started 👀 Results
user test this ✅ Started 👀 Results
run dt ✅ Started 👀 Results
perf test this faster ✅ Started 👀 Results

@typescript-bot
Copy link
Collaborator

Hey @jakebailey, the results of running the DT tests are ready.

There were interesting changes:

Branch only errors:

Package: ramda
Error:

Error: 
/mnt/vss/_work/1/DefinitelyTyped/types/ramda/test/map-tests.ts
  7:38  error  TypeScript@local compile error: 
No overload matches this call.
  The last overload gave the following error.
    Type 'number' is not assignable to type 'Iterable<unknown>'.
      Property '[Symbol.iterator]' is missing in type '{}' but required in type 'Iterable<unknown>'  @definitelytyped/expect
  7:44  error  TypeScript@local compile error: 
No overload matches this call.
  The last overload gave the following error.
    Type 'number' is not assignable to type 'Iterable<unknown>'.
      Property '[Symbol.iterator]' is missing in type '{}' but required in type 'Iterable<unknown>'  @definitelytyped/expect

✖ 2 problems (2 errors, 0 warnings)

    at combineErrorsAndWarnings (/mnt/vss/_work/1/DefinitelyTyped/node_modules/.pnpm/@definitelytyped+dtslint@0.2.22_typescript@5.6.0-dev.20240724/node_modules/@definitelytyped/dtslint/dist/index.js:194:28)
    at runTests (/mnt/vss/_work/1/DefinitelyTyped/node_modules/.pnpm/@definitelytyped+dtslint@0.2.22_typescript@5.6.0-dev.20240724/node_modules/@definitelytyped/dtslint/dist/index.js:186:20)

Package: node
Error:

Error: 
/mnt/vss/_work/1/DefinitelyTyped/types/node/test/stream.ts
  659:5  error  TypeScript@local tsconfig.dom.json, local tsconfig.non-dom.json expected type to be:
  Promise<any[] | undefined>
got:
  Promise<readonly unknown[] | undefined>  @definitelytyped/expect

✖ 1 problem (1 error, 0 warnings)

    at combineErrorsAndWarnings (/mnt/vss/_work/1/DefinitelyTyped/node_modules/.pnpm/@definitelytyped+dtslint@0.2.22_typescript@5.6.0-dev.20240724/node_modules/@definitelytyped/dtslint/dist/index.js:194:28)
    at runTests (/mnt/vss/_work/1/DefinitelyTyped/node_modules/.pnpm/@definitelytyped+dtslint@0.2.22_typescript@5.6.0-dev.20240724/node_modules/@definitelytyped/dtslint/dist/index.js:186:20)

You can check the log here.

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user tests with tsc comparing main and refs/pull/48228/merge:

Something interesting changed - please have a look.

Details

effect

packages/schema/benchmark/tsconfig.json

tsconfig.json

tsconfig.build.json

tsconfig.base.json

@typescript-bot
Copy link
Collaborator

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

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Errors 30 30 ~ ~ ~ p=1.000 n=6
Symbols 62,153 62,170 +17 (+ 0.03%) ~ ~ p=0.001 n=6
Types 50,242 50,246 +4 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 194,193k (± 1.03%) 193,555k (± 0.92%) ~ 192,297k 195,925k p=0.230 n=6
Parse Time 1.30s (± 0.75%) 1.30s (± 1.23%) ~ 1.27s 1.31s p=0.931 n=6
Bind Time 0.71s 0.71s ~ ~ ~ p=1.000 n=6
Check Time 9.56s (± 0.50%) 9.54s (± 0.57%) ~ 9.48s 9.63s p=0.517 n=6
Emit Time 2.75s (± 0.36%) 2.72s (± 0.79%) -0.03s (- 0.91%) 2.70s 2.76s p=0.042 n=6
Total Time 14.32s (± 0.28%) 14.27s (± 0.39%) ~ 14.22s 14.38s p=0.053 n=6
angular-1 - node (v18.15.0, x64)
Errors 7 7 ~ ~ ~ p=1.000 n=6
Symbols 945,532 946,486 +954 (+ 0.10%) ~ ~ p=0.001 n=6
Types 409,507 410,263 +756 (+ 0.18%) ~ ~ p=0.001 n=6
Memory used 1,221,174k (± 0.00%) 1,222,078k (± 0.01%) +904k (+ 0.07%) 1,221,971k 1,222,178k p=0.005 n=6
Parse Time 6.61s (± 0.73%) 6.59s (± 0.42%) ~ 6.54s 6.62s p=0.418 n=6
Bind Time 1.86s (± 0.65%) 1.86s (± 0.34%) ~ 1.85s 1.87s p=0.673 n=6
Check Time 31.18s (± 0.48%) 31.09s (± 0.36%) ~ 30.94s 31.24s p=0.261 n=6
Emit Time 15.01s (± 0.48%) 15.00s (± 0.56%) ~ 14.91s 15.09s p=0.935 n=6
Total Time 54.66s (± 0.35%) 54.54s (± 0.16%) ~ 54.46s 54.67s p=0.173 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,324,875 2,325,177 +302 (+ 0.01%) ~ ~ p=0.001 n=6
Types 949,556 949,698 +142 (+ 0.01%) ~ ~ p=0.001 n=6
Memory used 2,222,596k (± 0.00%) 2,222,894k (± 0.00%) +298k (+ 0.01%) 2,222,861k 2,222,945k p=0.005 n=6
Parse Time 6.65s (± 0.21%) 6.63s (± 0.20%) -0.02s (- 0.28%) 6.61s 6.65s p=0.048 n=6
Bind Time 2.32s (± 0.22%) 2.33s (± 0.70%) ~ 2.31s 2.35s p=0.276 n=6
Check Time 73.06s (± 0.14%) 73.13s (± 0.36%) ~ 72.87s 73.59s p=1.000 n=6
Emit Time 0.14s (± 3.77%) 0.14s ~ ~ ~ p=0.174 n=6
Total Time 82.17s (± 0.13%) 82.23s (± 0.29%) ~ 82.01s 82.67s p=0.810 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,230,046 1,230,222 +176 (+ 0.01%) ~ ~ p=0.001 n=6
Types 265,645 265,751 +106 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 2,347,376k (± 0.02%) 2,347,588k (± 0.02%) ~ 2,347,109k 2,348,308k p=0.575 n=6
Parse Time 4.99s (± 1.02%) 5.00s (± 0.93%) ~ 4.96s 5.06s p=0.470 n=6
Bind Time 1.90s (± 0.43%) 1.91s (± 1.12%) ~ 1.89s 1.95s p=0.270 n=6
Check Time 34.71s (± 0.32%) 34.68s (± 0.57%) ~ 34.46s 34.89s p=0.810 n=6
Emit Time 3.29s (± 0.79%) 3.28s (± 1.61%) ~ 3.19s 3.35s p=0.630 n=6
Total Time 44.90s (± 0.23%) 44.90s (± 0.51%) ~ 44.58s 45.15s p=0.936 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,230,046 1,230,222 +176 (+ 0.01%) ~ ~ p=0.001 n=6
Types 265,645 265,751 +106 (+ 0.04%) ~ ~ p=0.001 n=6
Memory used 2,421,996k (± 0.04%) 2,422,079k (± 0.01%) ~ 2,421,780k 2,422,282k p=0.810 n=6
Parse Time 6.20s (± 0.73%) 6.19s (± 0.26%) ~ 6.17s 6.21s p=0.810 n=6
Bind Time 2.03s (± 0.67%) 2.06s (± 0.80%) +0.03s (+ 1.40%) 2.03s 2.08s p=0.019 n=6
Check Time 41.27s (± 0.38%) 41.26s (± 0.38%) ~ 41.12s 41.55s p=0.748 n=6
Emit Time 3.98s (± 1.54%) 4.09s (± 2.52%) +0.11s (+ 2.85%) 4.01s 4.30s p=0.020 n=6
Total Time 53.49s (± 0.35%) 53.60s (± 0.32%) ~ 53.46s 53.82s p=0.092 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 258,471 258,502 +31 (+ 0.01%) ~ ~ p=0.001 n=6
Types 105,534 105,570 +36 (+ 0.03%) ~ ~ p=0.001 n=6
Memory used 429,101k (± 0.01%) 429,458k (± 0.06%) +358k (+ 0.08%) 429,069k 429,761k p=0.031 n=6
Parse Time 3.35s (± 0.77%) 3.36s (± 0.78%) ~ 3.31s 3.38s p=0.254 n=6
Bind Time 1.33s (± 1.31%) 1.33s (± 0.74%) ~ 1.32s 1.34s p=0.315 n=6
Check Time 18.03s (± 0.35%) 17.98s (± 0.23%) ~ 17.93s 18.03s p=0.295 n=6
Emit Time 1.63s (± 0.85%) 1.64s (± 1.37%) ~ 1.61s 1.67s p=0.124 n=6
Total Time 24.32s (± 0.35%) 24.32s (± 0.14%) ~ 24.26s 24.35s p=0.936 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 35 35 ~ ~ ~ p=1.000 n=6
Symbols 224,931 224,969 +38 (+ 0.02%) ~ ~ p=0.001 n=6
Types 94,146 94,191 +45 (+ 0.05%) ~ ~ p=0.001 n=6
Memory used 370,163k (± 0.04%) 370,051k (± 0.02%) ~ 369,951k 370,172k p=0.173 n=6
Parse Time 3.43s (± 0.63%) 3.44s (± 0.59%) ~ 3.41s 3.47s p=0.685 n=6
Bind Time 1.92s (± 0.86%) 1.93s (± 0.87%) ~ 1.90s 1.94s p=0.154 n=6
Check Time 19.34s (± 0.21%) 19.34s (± 0.44%) ~ 19.23s 19.43s p=0.748 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 24.69s (± 0.25%) 24.71s (± 0.31%) ~ 24.60s 24.79s p=0.747 n=6
vscode - node (v18.15.0, x64)
Errors 11 11 ~ ~ ~ p=1.000 n=6
Symbols 2,985,984 2,989,692 +3,708 (+ 0.12%) ~ ~ p=0.001 n=6
Types 1,027,145 1,030,096 +2,951 (+ 0.29%) ~ ~ p=0.001 n=6
Memory used 3,110,036k (± 0.00%) 3,113,574k (± 0.00%) +3,539k (+ 0.11%) 3,113,450k 3,113,634k p=0.005 n=6
Parse Time 17.19s (± 0.38%) 17.13s (± 0.23%) ~ 17.10s 17.19s p=0.126 n=6
Bind Time 5.25s (± 1.73%) 5.26s (± 2.07%) ~ 5.20s 5.48s p=0.870 n=6
Check Time 96.24s (± 0.28%) 96.46s (± 0.56%) ~ 95.91s 97.43s p=0.689 n=6
Emit Time 24.90s (± 0.91%) 25.05s (± 0.58%) ~ 24.91s 25.29s p=0.066 n=6
Total Time 143.58s (± 0.34%) 143.91s (± 0.39%) ~ 143.15s 144.80s p=0.378 n=6
webpack - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 267,560 268,161 +601 (+ 0.22%) ~ ~ p=0.001 n=6
Types 109,076 109,363 +287 (+ 0.26%) ~ ~ p=0.001 n=6
Memory used 412,418k (± 0.02%) 412,877k (± 0.01%) +459k (+ 0.11%) 412,812k 412,918k p=0.005 n=6
Parse Time 3.85s (± 0.45%) 3.84s (± 0.55%) ~ 3.81s 3.86s p=0.368 n=6
Bind Time 1.72s 1.71s (± 0.68%) -0.01s (- 0.68%) 1.69s 1.72s p=0.028 n=6
Check Time 16.88s (± 0.29%) 16.88s (± 0.31%) ~ 16.80s 16.93s p=0.810 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 22.45s (± 0.23%) 22.42s (± 0.29%) ~ 22.32s 22.49s p=0.470 n=6
xstate-main - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 511,803 511,827 +24 (+ 0.00%) ~ ~ p=0.001 n=6
Types 162,088 162,115 +27 (+ 0.02%) ~ ~ p=0.001 n=6
Memory used 449,133k (± 0.06%) 449,092k (± 0.08%) ~ 448,621k 449,441k p=0.936 n=6
Parse Time 3.17s (± 1.03%) 3.17s (± 0.62%) ~ 3.14s 3.20s p=1.000 n=6
Bind Time 1.16s (± 0.44%) 1.15s (± 1.30%) ~ 1.14s 1.18s p=0.113 n=6
Check Time 17.18s (± 0.35%) 17.16s (± 0.48%) ~ 17.04s 17.23s p=0.936 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 21.51s (± 0.43%) 21.48s (± 0.44%) ~ 21.35s 21.61s p=0.747 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top 400 repos with tsc comparing main and refs/pull/48228/merge:

Something interesting changed - please have a look.

Details

apollographql/apollo-client

2 of 12 projects failed to build with the old tsc and were ignored

tsconfig.json

discordjs/discord.js

45 of 62 projects failed to build with the old tsc and were ignored

packages/formatters/tsconfig.json

packages/formatters/tsconfig.eslint.json

packages/formatters/tsconfig.docs.json

hexojs/hexo

tsconfig.json

  • error TS2339: Property 'push' does not exist on type 'string[] | (string & readonly string[])'.

honojs/hono

5 of 6 projects failed to build with the old tsc and were ignored

tsconfig.json

  • error TS2345: Argument of type '(string | HtmlEscapedString) & readonly string[]' is not assignable to parameter of type '(string | number | HtmlEscapedString)[]'.

pubkey/rxdb

8 of 11 projects failed to build with the old tsc and were ignored

tsconfig.json

config/tsconfig.types.json

recharts/recharts

1 of 4 projects failed to build with the old tsc and were ignored

tsconfig.json

storybook/tsconfig.json

demo/tsconfig.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Status: Waiting on reviewers
6 participants