From aa518b7cadf846bce0882e71ebcaedf2bf2fcecb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:30:00 +0800 Subject: [PATCH] Update flow-parser to v0.317.0 (#19285) Co-authored-by: fisker --- package.json | 2 +- tests/config/format-test/constants.js | 2 +- tests/config/format-test/get-fixtures.js | 4 +- tests/config/format-test/run-format-test.js | 6 +- .../__snapshots__/format.test.js.snap | 2119 +++++++++++++++++ .../flow/flow-repo/mapped_types/aliases.js | 16 + .../flow/flow-repo/mapped_types/deep_types.js | 57 + .../flow-repo/mapped_types/distributivity.js | 30 + .../flow/flow-repo/mapped_types/export.js | 13 + .../flow-repo/mapped_types/format.test.js | 1 + .../flow/flow-repo/mapped_types/identity.js | 52 + .../flow-repo/mapped_types/idx_replacement.js | 94 + .../flow/flow-repo/mapped_types/import.js | 90 + .../flow/flow-repo/mapped_types/instancet.js | 42 + .../flow/flow-repo/mapped_types/issue-2674.js | 7 + .../flow/flow-repo/mapped_types/key_mirror.js | 9 + .../lti_implicit_instantiation.js | 9 + .../flow-repo/mapped_types/minus_optional.js | 67 + .../mapped_types/no_explicit_exactness.js | 14 + .../flow-repo/mapped_types/no_other_props.js | 25 + .../flow-repo/mapped_types/non_homomorphic.js | 50 + .../non_homomorphic_resolution.js | 29 + .../flow-repo/mapped_types/pick_and_omit.js | 84 + .../flow/flow-repo/mapped_types/promises.js | 9 + .../flow/flow-repo/mapped_types/reasons.js | 13 + .../mapped_types/semi_homomorphic.js | 47 + .../flow/flow-repo/mapped_types/test.js | 141 ++ .../flow/flow-repo/mapped_types/union.js | 9 + yarn.lock | 10 +- 29 files changed, 3039 insertions(+), 12 deletions(-) create mode 100644 tests/format/flow/flow-repo/mapped_types/__snapshots__/format.test.js.snap create mode 100644 tests/format/flow/flow-repo/mapped_types/aliases.js create mode 100644 tests/format/flow/flow-repo/mapped_types/deep_types.js create mode 100644 tests/format/flow/flow-repo/mapped_types/distributivity.js create mode 100644 tests/format/flow/flow-repo/mapped_types/export.js create mode 100644 tests/format/flow/flow-repo/mapped_types/format.test.js create mode 100644 tests/format/flow/flow-repo/mapped_types/identity.js create mode 100644 tests/format/flow/flow-repo/mapped_types/idx_replacement.js create mode 100644 tests/format/flow/flow-repo/mapped_types/import.js create mode 100644 tests/format/flow/flow-repo/mapped_types/instancet.js create mode 100644 tests/format/flow/flow-repo/mapped_types/issue-2674.js create mode 100644 tests/format/flow/flow-repo/mapped_types/key_mirror.js create mode 100644 tests/format/flow/flow-repo/mapped_types/lti_implicit_instantiation.js create mode 100644 tests/format/flow/flow-repo/mapped_types/minus_optional.js create mode 100644 tests/format/flow/flow-repo/mapped_types/no_explicit_exactness.js create mode 100644 tests/format/flow/flow-repo/mapped_types/no_other_props.js create mode 100644 tests/format/flow/flow-repo/mapped_types/non_homomorphic.js create mode 100644 tests/format/flow/flow-repo/mapped_types/non_homomorphic_resolution.js create mode 100644 tests/format/flow/flow-repo/mapped_types/pick_and_omit.js create mode 100644 tests/format/flow/flow-repo/mapped_types/promises.js create mode 100644 tests/format/flow/flow-repo/mapped_types/reasons.js create mode 100644 tests/format/flow/flow-repo/mapped_types/semi_homomorphic.js create mode 100644 tests/format/flow/flow-repo/mapped_types/test.js create mode 100644 tests/format/flow/flow-repo/mapped_types/union.js diff --git a/package.json b/package.json index 0184f63e3..e6989bfea 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "fast-json-stable-stringify": "2.1.0", "file-entry-cache": "11.1.3", "find-cache-directory": "6.0.0", - "flow-parser": "0.316.0", + "flow-parser": "0.317.0", "get-east-asian-width": "1.6.0", "get-stdin": "9.0.0", "graphql": "16.14.1", diff --git a/tests/config/format-test/constants.js b/tests/config/format-test/constants.js index c2d45137f..9d7da361f 100644 --- a/tests/config/format-test/constants.js +++ b/tests/config/format-test/constants.js @@ -4,7 +4,7 @@ import { normalizeDirectory } from "./utilities.js"; const { __dirname } = createEsmUtils(import.meta); -export const FORMAT_SCRIPT_FILENAME = "format.test.js"; +export const FORMAT_TEST_SCRIPT_FILENAME = "format.test.js"; export const FORMAT_TEST_DIRECTORY = normalizeDirectory( path.join(__dirname, "../../format/"), diff --git a/tests/config/format-test/get-fixtures.js b/tests/config/format-test/get-fixtures.js index 2f0745c0c..37adafb0b 100644 --- a/tests/config/format-test/get-fixtures.js +++ b/tests/config/format-test/get-fixtures.js @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { FORMAT_SCRIPT_FILENAME } from "./constants.js"; +import { FORMAT_TEST_SCRIPT_FILENAME } from "./constants.js"; import { verifyFilename } from "./verify-fixtures.js"; import visualizeEndOfLine from "./visualize-end-of-line.js"; @@ -28,7 +28,7 @@ function* getFiles(context) { if ( !file.isFile() || filename[0] === "." || - filename === FORMAT_SCRIPT_FILENAME || + filename === FORMAT_TEST_SCRIPT_FILENAME || // VSCode creates this file sometime https://github.com/microsoft/vscode/issues/105191 filename === "debug.log" || path.extname(filename) === ".snap" diff --git a/tests/config/format-test/run-format-test.js b/tests/config/format-test/run-format-test.js index cbf3da15b..fe5147818 100644 --- a/tests/config/format-test/run-format-test.js +++ b/tests/config/format-test/run-format-test.js @@ -1,6 +1,6 @@ import path from "node:path"; import url from "node:url"; -import { FORMAT_SCRIPT_FILENAME } from "./constants.js"; +import { FORMAT_TEST_SCRIPT_FILENAME } from "./constants.js"; import { getFixtures } from "./get-fixtures.js"; import { getParsers } from "./get-parsers.js"; import { testFixture } from "./run-test.js"; @@ -41,9 +41,9 @@ function runFormatTest(rawFixtures, explicitParsers, rawOptions) { : { importMeta: rawFixtures }; const filename = path.basename(new URL(importMeta.url).pathname); - if (filename !== FORMAT_SCRIPT_FILENAME) { + if (filename !== FORMAT_TEST_SCRIPT_FILENAME) { throw new Error( - `Format test should run in file named '${FORMAT_SCRIPT_FILENAME}'.`, + `Format test should run in file named '${FORMAT_TEST_SCRIPT_FILENAME}'.`, ); } diff --git a/tests/format/flow/flow-repo/mapped_types/__snapshots__/format.test.js.snap b/tests/format/flow/flow-repo/mapped_types/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..068a9a6c1 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/__snapshots__/format.test.js.snap @@ -0,0 +1,2119 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`aliases.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +// Mapped Type aliases should not participate in the buggy TypeAppT ~> TypeAppT special case +type ReadOnly = {readonly [key in keyof O]: O[key]}; +type ReadOnlyDeeper = ReadOnly; +type ReadOnlyDeepest = ReadOnlyDeeper; + +type O1 = {foo: number}; +type O2 = {foo: string | number}; + +declare const x: ReadOnly; +x as ReadOnly; // OK + +declare const y: ReadOnlyDeeper; +y as ReadOnlyDeeper; // OK + +declare const z: ReadOnlyDeepest; +z as ReadOnlyDeepest; // OK + +=====================================output===================================== +// Mapped Type aliases should not participate in the buggy TypeAppT ~> TypeAppT special case +type ReadOnly = { readonly [key in keyof O]: O[key] }; +type ReadOnlyDeeper = ReadOnly; +type ReadOnlyDeepest = ReadOnlyDeeper; + +type O1 = { foo: number }; +type O2 = { foo: string | number }; + +declare const x: ReadOnly; +x as ReadOnly; // OK + +declare const y: ReadOnlyDeeper; +y as ReadOnlyDeeper; // OK + +declare const z: ReadOnlyDeepest; +z as ReadOnlyDeepest; // OK + +================================================================================ +`; + +exports[`deep_types.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type DeepPartialState = + // Leave primitives alone + T extends ?(number | string | boolean) + ? T + : Readonly<{ + [K in keyof T]: DeepPartialState[K]>, + }> + +{ + declare const a: DeepPartialState; + a as number; // ok + a as empty; // error: number ~> empty + + declare const b: DeepPartialState<{foo: {bar: {baz: string}}}>; + b as {readonly foo?: {readonly bar?: {readonly baz?: string}}}; + b as {readonly foo: {readonly bar?: {readonly baz?: string}}}; // error: foo not optional + b as {readonly foo?: {readonly bar: {readonly baz?: string}}}; // error: bar not optional + b as {readonly foo?: {readonly bar?: {readonly baz: string}}}; // error: baz not optional +} + +type DeepReadOnly = + T extends ReadonlyArray ? ReadonlyArray> : + T extends {...} ? {readonly [K in keyof T]: DeepReadOnly} : T; + +{ + const obj: DeepReadOnly<{ + arr: Array<{value: string, ...}>, + nested: { + value: string, + doubleNested: {value: string, ...}, + ... + }, + ... + }> = { + arr: [{value: 'a'}, {value: 'b'}], + nested: { + value: 'nested value', + doubleNested: { + value: 'double nested value', + }, + }, + value: 'top value', + }; + obj as empty; // error + + obj.arr.push({value: 'updated value'}); // error: Cannot mutate arrays. + obj.arr[0].value = 'updated value'; // error: Cannot mutate objects in arrays. + obj.arr = []; // error: Cannot change top-level values. + obj.nested = obj.nested; // error: Cannot change top-level values. + obj.nested.value = 'updated value'; // error: Cannot change nested values. + obj.nested.doubleNested = obj.nested.doubleNested; // error: Cannot change nested values. + obj.nested.doubleNested.value = 'updated value'; // error: Cannot change nested values. + + const arr: DeepReadOnly> = ['hello', 'world']; + arr.push('goodbye'); // error: Cannot mutate arrays. + arr[0] = 'goodbye'; // error: Cannot mutate values in arrays. +} + +=====================================output===================================== +type DeepPartialState = + // Leave primitives alone + T extends ?(number | string | boolean) + ? T + : Readonly<{ + [K in keyof T]: DeepPartialState[K]>, + }>; + +{ + declare const a: DeepPartialState; + a as number; // ok + a as empty; // error: number ~> empty + + declare const b: DeepPartialState<{ foo: { bar: { baz: string } } }>; + b as { readonly foo?: { readonly bar?: { readonly baz?: string } } }; + b as { readonly foo: { readonly bar?: { readonly baz?: string } } }; // error: foo not optional + b as { readonly foo?: { readonly bar: { readonly baz?: string } } }; // error: bar not optional + b as { readonly foo?: { readonly bar?: { readonly baz: string } } }; // error: baz not optional +} + +type DeepReadOnly = + T extends ReadonlyArray + ? ReadonlyArray> + : T extends { ... } + ? { readonly [K in keyof T]: DeepReadOnly } + : T; + +{ + const obj: DeepReadOnly<{ + arr: Array<{ value: string, ... }>, + nested: { + value: string, + doubleNested: { value: string, ... }, + ... + }, + ... + }> = { + arr: [{ value: "a" }, { value: "b" }], + nested: { + value: "nested value", + doubleNested: { + value: "double nested value", + }, + }, + value: "top value", + }; + obj as empty; // error + + obj.arr.push({ value: "updated value" }); // error: Cannot mutate arrays. + obj.arr[0].value = "updated value"; // error: Cannot mutate objects in arrays. + obj.arr = []; // error: Cannot change top-level values. + obj.nested = obj.nested; // error: Cannot change top-level values. + obj.nested.value = "updated value"; // error: Cannot change nested values. + obj.nested.doubleNested = obj.nested.doubleNested; // error: Cannot change nested values. + obj.nested.doubleNested.value = "updated value"; // error: Cannot change nested values. + + const arr: DeepReadOnly> = ["hello", "world"]; + arr.push("goodbye"); // error: Cannot mutate arrays. + arr[0] = "goodbye"; // error: Cannot mutate values in arrays. +} + +================================================================================ +`; + +exports[`distributivity.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +// Tests that we correctly distribute mapped types over unions. +// A failure here would be trying to access a key from a branch +// that only exists in one branch of the union. +{ + type Homomorphic = {[key in keyof O]: O[key]}; + declare const o: Homomorphic<{foo: number} | {bar: number}>; // OK + o as {foo: number} | {bar: number}; // OK + o as {foo: empty} | {bar: empty}; // ERROR x2 +} + +{ + type SemiHomomorphic = {[key in Keys]: O[key]}; + declare const o: SemiHomomorphic<{foo: number, bar: number} | {foo: string, baz: number}, 'foo'>; + o as {foo: number} | {foo: string}; // OK + o as {}; // ERROR +} + +{ + type DistributeNullAndVoid = {writeonly [key in keyof O]: number}; + + type O = {foo: number}; + + declare const oExplicit: DistributeNullAndVoid; + oExplicit as {writeonly foo: number} | null | void; + oExplicit as {writeonly foo: number}; // ERROR 2x + + declare const oMaybe: DistributeNullAndVoid; + oMaybe as {writeonly foo: number} | null | void; + oMaybe as {writeonly foo: number}; // ERROR +} + +=====================================output===================================== +// Tests that we correctly distribute mapped types over unions. +// A failure here would be trying to access a key from a branch +// that only exists in one branch of the union. +{ + type Homomorphic = { [key in keyof O]: O[key] }; + declare const o: Homomorphic<{ foo: number } | { bar: number }>; // OK + o as { foo: number } | { bar: number }; // OK + o as { foo: empty } | { bar: empty }; // ERROR x2 +} + +{ + type SemiHomomorphic = { + [key in Keys]: O[key], + }; + declare const o: SemiHomomorphic< + { foo: number, bar: number } | { foo: string, baz: number }, + "foo", + >; + o as { foo: number } | { foo: string }; // OK + o as {}; // ERROR +} + +{ + type DistributeNullAndVoid = { writeonly [key in keyof O]: number }; + + type O = { foo: number }; + + declare const oExplicit: DistributeNullAndVoid; + oExplicit as { writeonly foo: number } | null | void; + oExplicit as { writeonly foo: number }; // ERROR 2x + + declare const oMaybe: DistributeNullAndVoid; + oMaybe as { writeonly foo: number } | null | void; + oMaybe as { writeonly foo: number }; // ERROR +} + +================================================================================ +`; + +exports[`export.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type O = {foo: number, bar?: string, readonly baz: boolean}; + +export type MappedO = {[key in keyof O]: O[key]}; +export type AddOptional = {[key in keyof O]?: O[key]}; +export type AllReadonly = {readonly [key in keyof O]: O[key]}; + +export type ParameterizedId = {[key in keyof O]: O[key]}; +export type ParameterizedPartial = {[key in keyof O]?: O[key]}; +export type ParameterizedReadonly = {readonly [key in keyof O]: O[key]}; + +export type MappedNonHomomorphic = {[key in 'foo' | 'bar']: number}; + +export type SemiHomomorphic = {[key in Keys]: O[key]}; + +=====================================output===================================== +type O = { foo: number, bar?: string, readonly baz: boolean }; + +export type MappedO = { [key in keyof O]: O[key] }; +export type AddOptional = { [key in keyof O]?: O[key] }; +export type AllReadonly = { readonly [key in keyof O]: O[key] }; + +export type ParameterizedId = { [key in keyof O]: O[key] }; +export type ParameterizedPartial = { + [key in keyof O]?: O[key], +}; +export type ParameterizedReadonly = { + readonly [key in keyof O]: O[key], +}; + +export type MappedNonHomomorphic = { [key in "foo" | "bar"]: number }; + +export type SemiHomomorphic = { + [key in Keys]: O[key], +}; + +================================================================================ +`; + +exports[`identity.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +declare const any: any; + +type IdentityMap = {[K in keyof O]: O[K]}; +type Foo = IdentityMap<{ a: number, b: number }>; +declare const foo: Foo; + +({ + a: 42, // OK + b: 42, // OK +} as Foo); + +({ + a: 'asd', // Error: string ~> number + b: 'asd', // Error: string ~> number +} as Foo); + +((any as { + a: string, // Error: string ~> number + b: string, // Error: string ~> number +}) as Foo); + +({} as {} as Foo); // Error: \`a\` and \`b\` are not defined. + +any as {} as Foo; // Error: \`a\` and \`b\` are not defined. + +(foo as { + a: number, // OK + b: number, // OK +}); + +(foo as { + a: string, // Error: number ~> string + b: string, // Error: number ~> string +}); + +foo.a as empty; // Error: number ~> empty + +({ + foo: 'asd', // OK +} as IdentityMap<{ foo: number | string }>); + +({ + foo: 'asd', // OK +} as IdentityMap<{ foo: number } | { foo: string }>); + +({ + foo: true, // Error: boolean ~> number | string +} as IdentityMap<{ foo: number | string }>); + +({ + foo: true, // Error: boolean ~> number | string +} as IdentityMap<{ foo: number } | { foo: string }>); + +=====================================output===================================== +declare const any: any; + +type IdentityMap = { [K in keyof O]: O[K] }; +type Foo = IdentityMap<{ a: number, b: number }>; +declare const foo: Foo; + +({ + a: 42, // OK + b: 42, // OK +}) as Foo; + +({ + a: "asd", // Error: string ~> number + b: "asd", // Error: string ~> number +}) as Foo; + +any as { + a: string, // Error: string ~> number + b: string, // Error: string ~> number +} as Foo; + +({}) as {} as Foo; // Error: \`a\` and \`b\` are not defined. + +any as {} as Foo; // Error: \`a\` and \`b\` are not defined. + +foo as { + a: number, // OK + b: number, // OK +}; + +foo as { + a: string, // Error: number ~> string + b: string, // Error: number ~> string +}; + +foo.a as empty; // Error: number ~> empty + +({ + foo: "asd", // OK +}) as IdentityMap<{ foo: number | string }>; + +({ + foo: "asd", // OK +}) as IdentityMap<{ foo: number } | { foo: string }>; + +({ + foo: true, // Error: boolean ~> number | string +}) as IdentityMap<{ foo: number | string }>; + +({ + foo: true, // Error: boolean ~> number | string +}) as IdentityMap<{ foo: number } | { foo: string }>; + +================================================================================ +`; + +exports[`idx_replacement.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +declare opaque type DeepRequiredArray: ReadonlyArray< + DeepRequired>, +>; + +declare opaque type DeepRequiredObject: Required<{ + readonly [K in keyof T]: DeepRequired, +}>; + +declare opaque type IdxNonMaybeType: NonNullable; + +declare opaque type DeepRequired: T extends empty + ? $FlowFixMe // If something can pass empty, it's already unsafe + : T extends ReadonlyArray + ? DeepRequiredArray + : T extends interface {} + ? DeepRequiredObject + : IdxNonMaybeType; + +type UnboxDeepRequired = T extends DeepRequired ? V : T; + +declare function idx( + prop: T1, + accessor: (prop: NonNullable>) => T2, +): ?UnboxDeepRequired; + +// Migrated from original idx test +{ + // Objects + declare const obj1: {a: ?{b: {c: number}}}; + obj1.a.b.c; // error + idx(obj1, obj => obj.a.b.c) as ?number; // ok + idx(obj1, obj => obj["a"].b.c) as ?number; // ok + idx(obj1, obj => obj.a.b.c) as number; // error: result must be MaybeT + idx(obj1, obj => obj.a.b.c) as ?string; // error: number ~> string + idx(obj1, obj => obj["a"].b.c) as number; // error: result must be MaybeT + idx(obj1, obj => obj.notAProp); // error: prop-missing + idx(obj1, obj => obj.a = null); // error: invalid-idx + declare const obj2: {a?: {b: {c: number}}}; + idx(obj2, obj => obj.a.b.c) as ?number; // ok + idx(obj2, obj => obj.a.b.c) as number; // error: result must be MaybeT + declare const obj3: {a: null | {b: {c: number}}}; + idx(obj3, obj => obj.a.b.c) as ?number; // ok + idx(obj3, obj => obj.a.b.c) as number; // error: result must be MaybeT + // Nested maybes/optionals should get unwrapped + declare const obj4: {a?: ?(?{b: number})}; + idx(obj4, obj => obj.a.b) as ?number; // ok + + // Unions + declare const ab: {a:string}|{b:number}; + idx(ab, _ => _.a) as empty; // error + idx(ab, _ => _.b) as empty; // error + idx(ab, _ => _.c) as empty; // error + + // Classes + class Foo1 { a: ?Foo1; b: ?number; } + class Foo2 { a: Foo2 | void; b: ?number; } + class Foo3 { a: Foo3 | null; b: ?number; } + idx(new Foo1(), o => o.a.b) as ?number; // ok + idx(new Foo1(), o => o.a.b) as number; // error: result must be MaybeT + idx(new Foo1(), o => o.a = null); // error: not writable + + // Arrays + declare const arr1: Array>; + declare const arr2: Array | void>; + declare const arr3: Array | null>; + idx(arr1, arr => arr[0][0]) as ?number; // ok + idx(arr2, arr => arr[0][0]) as ?number; // ok + idx(arr3, arr => arr[0][0]) as ?number; // ok + + // Non-objects + idx(42, n => n) as ?number; // ok + idx(42, n => n) as number; // error: result must be MaybeT + idx(42, n => n.nope); // error: prop-missing + + // Weird edge cases + // Using an annotation obscures the type wrapper mechanism that idx() uses + // around the parameter it passes to the callback + idx({}, (obj: any) => obj.a.b.c) as ?number; // ok + // Can't do anything with the callback parameter other than get elements and properties off of it + idx({}, obj => obj()); // error +} + +// Additional tests +{ + declare const props: { data: { readonly id: ?string } }; + declare function nullthrows(x: ?T): T; + + const a = nullthrows(idx(props, _ => _.data.id)); + a as empty; // error + if (a != null) { + a as empty; // error + a as string; // ok + } +} + +=====================================output===================================== +declare opaque type DeepRequiredArray: ReadonlyArray< + DeepRequired>, +>; + +declare opaque type DeepRequiredObject: Required<{ + readonly [K in keyof T]: DeepRequired, +}>; + +declare opaque type IdxNonMaybeType: NonNullable; + +declare opaque type DeepRequired: T extends empty + ? $FlowFixMe // If something can pass empty, it's already unsafe + : T extends ReadonlyArray + ? DeepRequiredArray + : T extends interface {} + ? DeepRequiredObject + : IdxNonMaybeType; + +type UnboxDeepRequired = T extends DeepRequired ? V : T; + +declare function idx( + prop: T1, + accessor: (prop: NonNullable>) => T2, +): ?UnboxDeepRequired; + +// Migrated from original idx test +{ + // Objects + declare const obj1: { a: ?{ b: { c: number } } }; + obj1.a.b.c; // error + idx(obj1, (obj) => obj.a.b.c) as ?number; // ok + idx(obj1, (obj) => obj["a"].b.c) as ?number; // ok + idx(obj1, (obj) => obj.a.b.c) as number; // error: result must be MaybeT + idx(obj1, (obj) => obj.a.b.c) as ?string; // error: number ~> string + idx(obj1, (obj) => obj["a"].b.c) as number; // error: result must be MaybeT + idx(obj1, (obj) => obj.notAProp); // error: prop-missing + idx(obj1, (obj) => (obj.a = null)); // error: invalid-idx + declare const obj2: { a?: { b: { c: number } } }; + idx(obj2, (obj) => obj.a.b.c) as ?number; // ok + idx(obj2, (obj) => obj.a.b.c) as number; // error: result must be MaybeT + declare const obj3: { a: null | { b: { c: number } } }; + idx(obj3, (obj) => obj.a.b.c) as ?number; // ok + idx(obj3, (obj) => obj.a.b.c) as number; // error: result must be MaybeT + // Nested maybes/optionals should get unwrapped + declare const obj4: { a?: ??{ b: number } }; + idx(obj4, (obj) => obj.a.b) as ?number; // ok + + // Unions + declare const ab: { a: string } | { b: number }; + idx(ab, (_) => _.a) as empty; // error + idx(ab, (_) => _.b) as empty; // error + idx(ab, (_) => _.c) as empty; // error + + // Classes + class Foo1 { + a: ?Foo1; + b: ?number; + } + class Foo2 { + a: Foo2 | void; + b: ?number; + } + class Foo3 { + a: Foo3 | null; + b: ?number; + } + idx(new Foo1(), (o) => o.a.b) as ?number; // ok + idx(new Foo1(), (o) => o.a.b) as number; // error: result must be MaybeT + idx(new Foo1(), (o) => (o.a = null)); // error: not writable + + // Arrays + declare const arr1: Array>; + declare const arr2: Array | void>; + declare const arr3: Array | null>; + idx(arr1, (arr) => arr[0][0]) as ?number; // ok + idx(arr2, (arr) => arr[0][0]) as ?number; // ok + idx(arr3, (arr) => arr[0][0]) as ?number; // ok + + // Non-objects + idx(42, (n) => n) as ?number; // ok + idx(42, (n) => n) as number; // error: result must be MaybeT + idx(42, (n) => n.nope); // error: prop-missing + + // Weird edge cases + // Using an annotation obscures the type wrapper mechanism that idx() uses + // around the parameter it passes to the callback + idx({}, (obj: any) => obj.a.b.c) as ?number; // ok + // Can't do anything with the callback parameter other than get elements and properties off of it + idx({}, (obj) => obj()); // error +} + +// Additional tests +{ + declare const props: { data: { readonly id: ?string } }; + declare function nullthrows(x: ?T): T; + + const a = nullthrows(idx(props, (_) => _.data.id)); + a as empty; // error + if (a != null) { + a as empty; // error + a as string; // ok + } +} + +================================================================================ +`; + +exports[`import.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +import type { + MappedO, + AddOptional, + AllReadonly, + ParameterizedId, + ParameterizedPartial, + ParameterizedReadonly, + MappedNonHomomorphic, + SemiHomomorphic, +} from './export'; + +// No modifiers concrete +{ + ({foo: 3, bar: 'str', baz: true} as MappedO); // OK + + declare const mapped: MappedO; + mapped as {foo: number, bar?: string, readonly baz: boolean}; // OK + mapped as {foo: empty, bar: empty, readonly baz: empty}; // ERROR + mapped.baz = true; // ERROR +} + +// Add optional concrete +{ + ({foo: undefined, bar: undefined, baz: undefined} as AddOptional); // OK + declare const addOptional: AddOptional; + addOptional as {foo?: number, bar?: string, readonly baz?: boolean}; // OK + addOptional.baz = true; // ERROR +} + +// Add readonly concrete +{ + ({foo: 3, bar: undefined, baz: true} as AllReadonly); + declare const readonly: AllReadonly; + readonly.foo = 4; // ERROR; + readonly.bar = 'str'; // ERROR; + readonly.baz = false; // ERROR; +} + +// All of these tests are the same as above but use a parameterize type alias +type O = {foo: number, bar?: string, readonly baz: boolean}; +// No modifiers parameterized +{ + ({foo: 3, bar: 'str', baz: true} as ParameterizedId); // OK + + declare const mapped: ParameterizedId; + mapped as {foo: number, bar?: string, readonly baz: boolean}; // OK + mapped as {foo: empty, bar: empty, readonly baz: empty}; // ERROR + mapped.baz = true; // ERROR +} + +// Add optional parameterized +{ + ({foo: undefined, bar: undefined, baz: undefined} as ParameterizedPartial); // OK + declare const addOptional: ParameterizedPartial; + addOptional as {foo?: number, bar?: string, readonly baz?: boolean}; // OK + addOptional.baz = true; // ERROR +} + +// Add readonly parameterized +{ + ({foo: 3, bar: undefined, baz: true} as ParameterizedReadonly); + declare const readonly: ParameterizedReadonly; + readonly.foo = 4; // ERROR; + readonly.bar = 'str'; // ERROR; + readonly.baz = false; // ERROR; +} + +// Non-homomorphic mapped types +{ + ({foo: 3, bar: 3} as MappedNonHomomorphic); // OK! + ({foo: null, bar: null} as MappedNonHomomorphic); // ERROR! +} + +// Semi-homomorphic mapped types +{ + declare const semi: SemiHomomorphic<{readonly foo: number, bar: string}, 'foo'>; + semi as {readonly foo: number}; // OK + semi as {foo: number}; // ERROR +} + +// Both homomorphic and semi-homomorphic mapped types are distributive +{ + declare const semi: SemiHomomorphic<{readonly foo: number} | {readonly foo: string}, 'foo'>; + semi as {readonly foo: number} | {readonly foo: string}; // OK! + semi as {foo: number} | {foo: string}; // ERROR 2x + + declare const homomorphic: ParameterizedReadonly<{foo: number} | {bar:number} >; + homomorphic as {readonly foo: number} | {readonly bar: number}; // OK! + homomorphic as {foo: number} | {bar: number}; // ERROR 2x +} + +=====================================output===================================== +import type { + MappedO, + AddOptional, + AllReadonly, + ParameterizedId, + ParameterizedPartial, + ParameterizedReadonly, + MappedNonHomomorphic, + SemiHomomorphic, +} from "./export"; + +// No modifiers concrete +{ + ({ foo: 3, bar: "str", baz: true }) as MappedO; // OK + + declare const mapped: MappedO; + mapped as { foo: number, bar?: string, readonly baz: boolean }; // OK + mapped as { foo: empty, bar: empty, readonly baz: empty }; // ERROR + mapped.baz = true; // ERROR +} + +// Add optional concrete +{ + ({ foo: undefined, bar: undefined, baz: undefined }) as AddOptional; // OK + declare const addOptional: AddOptional; + addOptional as { foo?: number, bar?: string, readonly baz?: boolean }; // OK + addOptional.baz = true; // ERROR +} + +// Add readonly concrete +{ + ({ foo: 3, bar: undefined, baz: true }) as AllReadonly; + declare const readonly: AllReadonly; + readonly.foo = 4; // ERROR; + readonly.bar = "str"; // ERROR; + readonly.baz = false; // ERROR; +} + +// All of these tests are the same as above but use a parameterize type alias +type O = { foo: number, bar?: string, readonly baz: boolean }; +// No modifiers parameterized +{ + ({ foo: 3, bar: "str", baz: true }) as ParameterizedId; // OK + + declare const mapped: ParameterizedId; + mapped as { foo: number, bar?: string, readonly baz: boolean }; // OK + mapped as { foo: empty, bar: empty, readonly baz: empty }; // ERROR + mapped.baz = true; // ERROR +} + +// Add optional parameterized +{ + ({ + foo: undefined, + bar: undefined, + baz: undefined, + }) as ParameterizedPartial; // OK + declare const addOptional: ParameterizedPartial; + addOptional as { foo?: number, bar?: string, readonly baz?: boolean }; // OK + addOptional.baz = true; // ERROR +} + +// Add readonly parameterized +{ + ({ foo: 3, bar: undefined, baz: true }) as ParameterizedReadonly; + declare const readonly: ParameterizedReadonly; + readonly.foo = 4; // ERROR; + readonly.bar = "str"; // ERROR; + readonly.baz = false; // ERROR; +} + +// Non-homomorphic mapped types +{ + ({ foo: 3, bar: 3 }) as MappedNonHomomorphic; // OK! + ({ foo: null, bar: null }) as MappedNonHomomorphic; // ERROR! +} + +// Semi-homomorphic mapped types +{ + declare const semi: SemiHomomorphic< + { readonly foo: number, bar: string }, + "foo", + >; + semi as { readonly foo: number }; // OK + semi as { foo: number }; // ERROR +} + +// Both homomorphic and semi-homomorphic mapped types are distributive +{ + declare const semi: SemiHomomorphic< + { readonly foo: number } | { readonly foo: string }, + "foo", + >; + semi as { readonly foo: number } | { readonly foo: string }; // OK! + semi as { foo: number } | { foo: string }; // ERROR 2x + + declare const homomorphic: ParameterizedReadonly< + { foo: number } | { bar: number }, + >; + homomorphic as { readonly foo: number } | { readonly bar: number }; // OK! + homomorphic as { foo: number } | { bar: number }; // ERROR 2x +} + +================================================================================ +`; + +exports[`instancet.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +interface I { + foo: number; +} + +type MappedInterface = {readonly [key in keyof I]: I[key]}; +{ + declare const i: MappedInterface; + i as interface {readonly foo: number}; // OK + i as interface {foo: number}; // ERROR + i as {readonly foo: number, ...}; // ERROR (class object subtyping) +} + +interface WithIndexer { + foo: number; + [string]: boolean; +} +type MappedInterfaceWithIndexer = {readonly [key in keyof WithIndexer]: WithIndexer[key]}; +{ + declare const i: MappedInterfaceWithIndexer; + i as interface {readonly foo: number, readonly [string]: boolean}; // OK + i as interface {readonly foo: number, [string]: boolean}; // ERROR +} + +class A { + static bar: number; + foo: number; +} + +type MappedInstance = {readonly [key in keyof A]: A[key]}; +{ + declare const inst: MappedInstance; + inst as interface {readonly foo: number}; // OK + inst as interface {foo: number}; // ERROR + (inst['bar']); // ERROR +} + +type MappedClass = {readonly [key in keyof Class]: Class[key]}; +{ + declare const c: MappedClass; + c as interface {readonly bar: number}; // OK + c as interface {bar: number}; // ERROR +} + +=====================================output===================================== +interface I { + foo: number; +} + +type MappedInterface = { readonly [key in keyof I]: I[key] }; +{ + declare const i: MappedInterface; + i as interface { readonly foo: number }; // OK + i as interface { foo: number }; // ERROR + i as { readonly foo: number, ... }; // ERROR (class object subtyping) +} + +interface WithIndexer { + foo: number; + [string]: boolean; +} +type MappedInterfaceWithIndexer = { + readonly [key in keyof WithIndexer]: WithIndexer[key], +}; +{ + declare const i: MappedInterfaceWithIndexer; + i as interface { readonly foo: number, readonly [string]: boolean }; // OK + i as interface { readonly foo: number, [string]: boolean }; // ERROR +} + +class A { + static bar: number; + foo: number; +} + +type MappedInstance = { readonly [key in keyof A]: A[key] }; +{ + declare const inst: MappedInstance; + inst as interface { readonly foo: number }; // OK + inst as interface { foo: number }; // ERROR + inst["bar"]; // ERROR +} + +type MappedClass = { readonly [key in keyof Class]: Class[key] }; +{ + declare const c: MappedClass; + c as interface { readonly bar: number }; // OK + c as interface { bar: number }; // ERROR +} + +================================================================================ +`; + +exports[`issue-2674.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type Map = {[K in keyof O]: 'FOO'}; +type B = Map<{ FOO: null }>; +declare const b: B; + +b.FOO as 'FOO'; // ok +b.FOO as 'BAR'; // error +b.FOO = 'BAR'; // error + +=====================================output===================================== +type Map = { [K in keyof O]: "FOO" }; +type B = Map<{ FOO: null }>; +declare const b: B; + +b.FOO as "FOO"; // ok +b.FOO as "BAR"; // error +b.FOO = "BAR"; // error + +================================================================================ +`; + +exports[`key_mirror.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +declare function keyMirror(o: O): {[K in keyof O]: K}; + +var o = keyMirror({ + FOO: null, + BAR: null, +}); + +o.FOO as 'FOO'; // ok +o.FOO as 'BAR'; // error, 'FOO' incompatible with 'BAR' + +=====================================output===================================== +declare function keyMirror(o: O): { [K in keyof O]: K }; + +var o = keyMirror({ + FOO: null, + BAR: null, +}); + +o.FOO as "FOO"; // ok +o.FOO as "BAR"; // error, 'FOO' incompatible with 'BAR' + +================================================================================ +`; + +exports[`lti_implicit_instantiation.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type F = Readonly<{ + log: (() => T) => void, + ... + }>; + + declare function map(o1: O): {[K in keyof O]: O[K] extends F ? V : empty}; + + const foo = Object.freeze({bar: {log: (f: () => string) => {}}}) + const o1 = map(foo) as {readonly bar: number}; // error + +=====================================output===================================== +type F = Readonly<{ + log: (() => T) => void, + ... +}>; + +declare function map(o1: O): { + [K in keyof O]: O[K] extends F ? V : empty, +}; + +const foo = Object.freeze({ bar: { log: (f: () => string) => {} } }); +const o1 = map(foo) as { readonly bar: number }; // error + +================================================================================ +`; + +exports[`minus_optional.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type O = {foo: number, bar?: string}; + +// -? removes optionality from optional props +type Required1 = {[K in keyof O]-?: O[K]}; +declare const r1: Required1; +r1 as {foo: number, bar: string}; // OK +({foo: 1}) as Required1; // ERROR - bar is required + +// -? on already-required props is a no-op +type AllRequired = {foo: number, bar: string}; +type Same = {[K in keyof AllRequired]-?: AllRequired[K]}; +declare const s: Same; +s as AllRequired; // OK + +// Mimic Required utility +type MyRequired = {[K in keyof T]-?: T[K]}; +type X = MyRequired<{a?: number, b?: string, c: boolean}>; +declare const x: X; +x as {a: number, b: string, c: boolean}; // OK +({a: 1, c: true}) as X; // ERROR - b is required + +// Tuple input: -? clears per-element optional bit +type T = [a: string, b?: number, c?: boolean]; +type U = {[K in keyof T]-?: T[K]}; +declare const u: U; +u as [string, number, boolean]; // OK +(['']) as U; // ERROR - elements 1 and 2 are required + +// Nested: -? then +? roundtrip +type RoundTrip = {[K in keyof MyRequired]+?: MyRequired[K]}; +type Y = RoundTrip<{a?: number}>; +declare const y: Y; +y as {a?: number}; // OK + +// Required-prop with explicit \`void\` in the value type: -? does NOT strip void, +// since the source property was not optional. Matches TS. +type ReqWithVoid = {a: string | void}; +type ReqWithVoidR = MyRequired; +declare const rwv: ReqWithVoidR; +rwv.a as string | void; // OK +rwv.a as string; // ERROR - void survives because source prop was not optional + +// Required tuple element with \`void\` in the value type: -? does NOT strip void. +type ReqTupleWithVoid = [string | void]; +type ReqTupleWithVoidR = {[K in keyof ReqTupleWithVoid]-?: ReqTupleWithVoid[K]}; +declare const rtv: ReqTupleWithVoidR; +rtv[0] as string | void; // OK +rtv[0] as string; // ERROR - void survives + +// Plain Array element with \`void\`: -? DOES strip void from the array element type. +// TS: R> = Array. +type ArrWithVoid = Array; +type ArrWithVoidR = {[K in keyof ArrWithVoid]-?: ArrWithVoid[K]}; +declare const arr: ArrWithVoidR; +arr as Array; // OK - undefined stripped from array element +declare const a1: Array; +a1 as ArrWithVoidR; // ERROR - void no longer accepted + +// Tuple-wide elem_t: under -? the tuple-wide element type should no longer contain +// \`void\` contributed by originally-optional elements. Verified via computed +// numeric access (which reads elem_t, not the per-index element list). +type TWide = [a: string, b?: number, c?: boolean]; +type UWide = {[K in keyof TWide]-?: TWide[K]}; +declare const uWide: UWide; +declare const idx: number; +uWide[idx] as string | number | boolean; // OK - void filtered from elem_t +uWide.forEach((x) => { x as string | number | boolean; }); // OK - callback param uses elem_t + +=====================================output===================================== +type O = { foo: number, bar?: string }; + +// -? removes optionality from optional props +type Required1 = { [K in keyof O]-?: O[K] }; +declare const r1: Required1; +r1 as { foo: number, bar: string }; // OK +({ foo: 1 }) as Required1; // ERROR - bar is required + +// -? on already-required props is a no-op +type AllRequired = { foo: number, bar: string }; +type Same = { [K in keyof AllRequired]-?: AllRequired[K] }; +declare const s: Same; +s as AllRequired; // OK + +// Mimic Required utility +type MyRequired = { [K in keyof T]-?: T[K] }; +type X = MyRequired<{ a?: number, b?: string, c: boolean }>; +declare const x: X; +x as { a: number, b: string, c: boolean }; // OK +({ a: 1, c: true }) as X; // ERROR - b is required + +// Tuple input: -? clears per-element optional bit +type T = [a: string, b?: number, c?: boolean]; +type U = { [K in keyof T]-?: T[K] }; +declare const u: U; +u as [string, number, boolean]; // OK +[""] as U; // ERROR - elements 1 and 2 are required + +// Nested: -? then +? roundtrip +type RoundTrip = { [K in keyof MyRequired]+?: MyRequired[K] }; +type Y = RoundTrip<{ a?: number }>; +declare const y: Y; +y as { a?: number }; // OK + +// Required-prop with explicit \`void\` in the value type: -? does NOT strip void, +// since the source property was not optional. Matches TS. +type ReqWithVoid = { a: string | void }; +type ReqWithVoidR = MyRequired; +declare const rwv: ReqWithVoidR; +rwv.a as string | void; // OK +rwv.a as string; // ERROR - void survives because source prop was not optional + +// Required tuple element with \`void\` in the value type: -? does NOT strip void. +type ReqTupleWithVoid = [string | void]; +type ReqTupleWithVoidR = { + [K in keyof ReqTupleWithVoid]-?: ReqTupleWithVoid[K], +}; +declare const rtv: ReqTupleWithVoidR; +rtv[0] as string | void; // OK +rtv[0] as string; // ERROR - void survives + +// Plain Array element with \`void\`: -? DOES strip void from the array element type. +// TS: R> = Array. +type ArrWithVoid = Array; +type ArrWithVoidR = { [K in keyof ArrWithVoid]-?: ArrWithVoid[K] }; +declare const arr: ArrWithVoidR; +arr as Array; // OK - undefined stripped from array element +declare const a1: Array; +a1 as ArrWithVoidR; // ERROR - void no longer accepted + +// Tuple-wide elem_t: under -? the tuple-wide element type should no longer contain +// \`void\` contributed by originally-optional elements. Verified via computed +// numeric access (which reads elem_t, not the per-index element list). +type TWide = [a: string, b?: number, c?: boolean]; +type UWide = { [K in keyof TWide]-?: TWide[K] }; +declare const uWide: UWide; +declare const idx: number; +uWide[idx] as string | number | boolean; // OK - void filtered from elem_t +uWide.forEach((x) => { + x as string | number | boolean; +}); // OK - callback param uses elem_t + +================================================================================ +`; + +exports[`no_explicit_exactness.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +// Homomorphic mapped types cannot be explicitly exact or inexact. They take on +// the exactness of their input type + + +type O = {foo: number}; + +type MappedExact = {| // ERROR + [key in keyof O]: number +|} + +type MappedInexact = { // ERROR + [key in keyof O]: number, + ... +} + +=====================================output===================================== +// Homomorphic mapped types cannot be explicitly exact or inexact. They take on +// the exactness of their input type + +type O = { foo: number }; + +type MappedExact = {| + // ERROR + [key in keyof O]: number, +|}; + +type MappedInexact = { + // ERROR + [key in keyof O]: number, + ... +}; + +================================================================================ +`; + +exports[`no_other_props.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +// Mapped types cannot have extra properties yet + +type MappedFirst = { + [key in keyof {foo: number}]: number, // ERROR + prop: number, +}; +type SliceFirst = { + prop: number, + [key in keyof {foo: number}]: number, // ERROR +}; +type SpreadFirst = { + ...{prop: number}, + [key in keyof {foo: number}]: number, // ERROR +}; +type MappedLater1 = { + ...{prop: number}, + foo: number, + [key in keyof {foo: number}]: number, // ERROR +}; +type MappedLater2 = { + foo: number, + ...{prop: number}, + foo: number, + [key in keyof {foo: number}]: number, // ERROR +}; + +=====================================output===================================== +// Mapped types cannot have extra properties yet + +type MappedFirst = { + [key in keyof { foo: number }]: number, // ERROR + prop: number, +}; +type SliceFirst = { + prop: number, + [key in keyof { foo: number }]: number, // ERROR +}; +type SpreadFirst = { + ...{ prop: number }, + [key in keyof { foo: number }]: number, // ERROR +}; +type MappedLater1 = { + ...{ prop: number }, + foo: number, + [key in keyof { foo: number }]: number, // ERROR +}; +type MappedLater2 = { + foo: number, + ...{ prop: number }, + foo: number, + [key in keyof { foo: number }]: number, // ERROR +}; + +================================================================================ +`; + +exports[`non_homomorphic.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +{ + // Basic functionality test + type Keys = 'foo' | 'bar'; + type Mapped = {[key in Keys]: number}; + + declare const mapped: Mapped; + mapped.foo as number; // OK! + mapped.foo = 3; // OK! + + + mapped as empty; // ERROR! + mapped as {foo: empty, bar: empty}; // ERROR 2x + mapped as {foo: number, bar: number}; // OK! +} + +{ + // Leftover types are converted to indexers + type WithLeftovers = {[key in 'foo' | number]: number}; + declare const withLeftovers: WithLeftovers; + withLeftovers as {[number]: number, foo: number}; // OK! + withLeftovers as {[number]: string, foo: string}; // ERROR 2x +} + +{ + // Key types must be string | number | symbol, otherwise error on the + // definition. + // TS Reference: https://www.typescriptlang.org/play?ts=5.0.4#code/C4TwDgpgBAsghmSATKBeKBvA2gawiKASwDsoAjAewoBsI5iBdALimIFcBbMiAJwF8A3EA + type BadKeys = {[key in boolean]: number}; // ERROR! + declare const badKeys: BadKeys; + badKeys as any; +} + +{ + // Tests using mapped types in output positions for implicit instantiation + declare function keyMirror(...x: ReadonlyArray): {[key in T]: key}; + + let keyMirroredObject = keyMirror('foo', 'bar', 'baz'); + keyMirroredObject as {foo: 'foo', bar: 'bar', baz: 'baz'}; + + keyMirroredObject as {foo: 'bar', bar: 'baz', baz: 'foo'}; // ERROR x3 +} + +{ + // Tests using mapped types in input positions for implicit instantiation + declare function ObjWithKeys(x: {[key in T]: number}, ...y: ReadonlyArray): {[key in T]: number}; + const noContext = ObjWithKeys({foo: 3, bar: 3}, 'foo', 'bar'); // OK! + noContext as {foo: number, bar: number}; // OK! + noContext as {foo: empty, bar: empty, baz: number}; // ERROR 3x + const withReturnAnnot: {foo: number, bar: number} = ObjWithKeys({foo: 3, bar: 3}, 'foo', 'bar'); // OK! +} + +=====================================output===================================== +{ + // Basic functionality test + type Keys = "foo" | "bar"; + type Mapped = { [key in Keys]: number }; + + declare const mapped: Mapped; + mapped.foo as number; // OK! + mapped.foo = 3; // OK! + + mapped as empty; // ERROR! + mapped as { foo: empty, bar: empty }; // ERROR 2x + mapped as { foo: number, bar: number }; // OK! +} + +{ + // Leftover types are converted to indexers + type WithLeftovers = { [key in "foo" | number]: number }; + declare const withLeftovers: WithLeftovers; + withLeftovers as { [number]: number, foo: number }; // OK! + withLeftovers as { [number]: string, foo: string }; // ERROR 2x +} + +{ + // Key types must be string | number | symbol, otherwise error on the + // definition. + // TS Reference: https://www.typescriptlang.org/play?ts=5.0.4#code/C4TwDgpgBAsghmSATKBeKBvA2gawiKASwDsoAjAewoBsI5iBdALimIFcBbMiAJwF8A3EA + type BadKeys = { [key in boolean]: number }; // ERROR! + declare const badKeys: BadKeys; + badKeys as any; +} + +{ + // Tests using mapped types in output positions for implicit instantiation + declare function keyMirror(...x: ReadonlyArray): { + [key in T]: key, + }; + + let keyMirroredObject = keyMirror("foo", "bar", "baz"); + keyMirroredObject as { foo: "foo", bar: "bar", baz: "baz" }; + + keyMirroredObject as { foo: "bar", bar: "baz", baz: "foo" }; // ERROR x3 +} + +{ + // Tests using mapped types in input positions for implicit instantiation + declare function ObjWithKeys( + x: { [key in T]: number }, + ...y: ReadonlyArray + ): { [key in T]: number }; + const noContext = ObjWithKeys({ foo: 3, bar: 3 }, "foo", "bar"); // OK! + noContext as { foo: number, bar: number }; // OK! + noContext as { foo: empty, bar: empty, baz: number }; // ERROR 3x + const withReturnAnnot: { foo: number, bar: number } = ObjWithKeys( + { foo: 3, bar: 3 }, + "foo", + "bar", + ); // OK! +} + +================================================================================ +`; + +exports[`non_homomorphic_resolution.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +// This test checks ensures that we resolve TypeAppTs, KeysT, EvalTs, etc +// before trying to construct the mapped type. + +type Mapped = {[key in T]: number}; + +{ + type TypeApp = T; + type MappedTypeApp = Mapped>; + declare const mappedTypeApp: MappedTypeApp; + mappedTypeApp as empty; // ERROR + mappedTypeApp as {foo: number}; // OK! +} + +{ + type Keys = keyof {foo: number}; + type MappedKeys = Mapped; + declare const mappedKeys: MappedKeys; + mappedKeys as empty; // ERROR + mappedKeys as {foo: number}; // OK! +} + +{ + type O = {foo: 'foo'}; + type Eval = O['foo']; + type MappedEval = Mapped; + declare const mappedEval: MappedEval; + mappedEval as empty; // ERROR + mappedEval as {foo: number}; +} + +=====================================output===================================== +// This test checks ensures that we resolve TypeAppTs, KeysT, EvalTs, etc +// before trying to construct the mapped type. + +type Mapped = { [key in T]: number }; + +{ + type TypeApp = T; + type MappedTypeApp = Mapped>; + declare const mappedTypeApp: MappedTypeApp; + mappedTypeApp as empty; // ERROR + mappedTypeApp as { foo: number }; // OK! +} + +{ + type Keys = keyof { foo: number }; + type MappedKeys = Mapped; + declare const mappedKeys: MappedKeys; + mappedKeys as empty; // ERROR + mappedKeys as { foo: number }; // OK! +} + +{ + type O = { foo: "foo" }; + type Eval = O["foo"]; + type MappedEval = Mapped; + declare const mappedEval: MappedEval; + mappedEval as empty; // ERROR + mappedEval as { foo: number }; +} + +================================================================================ +`; + +exports[`pick_and_omit.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +//@flow +{ + // Omit Tests + type Exclude = T extends U ? empty : T; + + type Omit = { + [key in Exclude]: O[key], + }; + + type O = { + foo: number, + bar: number, + baz: number, + }; + + declare const noFoo: Omit; + noFoo.foo = 3; // ERROR! + noFoo as {bar: number, baz: number}; // OK! + + declare const noFooOrBar: Omit; + noFooOrBar as {baz: number}; // OK! + + declare const noKeysBad: Omit; // ERROR! string is not a subtype of keyof O + noKeysBad as {}; + + declare const noKeysGood: Omit; + noKeysGood as {}; // OK! + + declare function omit( + o: O, + ...keys: ReadonlyArray + ): Omit; + // KP: We need proper natural inference to make this work with no annotations + const noBarOrBaz = omit( + {foo: 3, bar: 3, baz: 3}, + 'bar' as 'bar', + 'baz' as 'baz', + ); + noBarOrBaz as {foo: number}; // OK! + noBarOrBaz.foo as number; // OK! + noBarOrBaz.foo = 3; // OK! + + declare function omitInput( + o: O, + x: Omit, + ...keys: ReadonlyArray + ): void; + const noIssues = omitInput({foo: 3, bar: 3}, {bar: 3}, 'foo' as 'foo'); + const badCall = omitInput({foo: 3, bar: 3}, {foo: 3}, 'foo' as 'foo'); // ERROR 2x +} + +{ + // Pick Tests + type Pick = {[key in Keys]: O[key]}; + + type P = { + foo: number, + bar: number, + baz: number, + }; + + declare const onlyFoo: Pick; + onlyFoo.bar; // ERROR! + onlyFoo as {foo: number}; // OK! + + declare const fooAndBaz: Pick; + fooAndBaz as {foo: number, baz: number}; // OK! + + declare function pick( + o: O, + ...keys: ReadonlyArray + ): Pick; + const picked = pick({foo: 3, bar: 3}, 'foo'); + picked as {foo: number}; + + declare function pickInput( + o: O, + x: Pick, + ...keys: ReadonlyArray + ): void; + + const noIssues = pickInput({foo: 3, bar: 3}, {foo: 3}, 'foo'); + const badCall = pickInput({foo: 3, bar: 3}, {bar: 3}, 'foo'); // ERROR 2x +} + +=====================================output===================================== +//@flow +{ + // Omit Tests + type Exclude = T extends U ? empty : T; + + type Omit = { + [key in Exclude]: O[key], + }; + + type O = { + foo: number, + bar: number, + baz: number, + }; + + declare const noFoo: Omit; + noFoo.foo = 3; // ERROR! + noFoo as { bar: number, baz: number }; // OK! + + declare const noFooOrBar: Omit; + noFooOrBar as { baz: number }; // OK! + + declare const noKeysBad: Omit; // ERROR! string is not a subtype of keyof O + noKeysBad as {}; + + declare const noKeysGood: Omit; + noKeysGood as {}; // OK! + + declare function omit( + o: O, + ...keys: ReadonlyArray + ): Omit; + // KP: We need proper natural inference to make this work with no annotations + const noBarOrBaz = omit( + { foo: 3, bar: 3, baz: 3 }, + "bar" as "bar", + "baz" as "baz", + ); + noBarOrBaz as { foo: number }; // OK! + noBarOrBaz.foo as number; // OK! + noBarOrBaz.foo = 3; // OK! + + declare function omitInput( + o: O, + x: Omit, + ...keys: ReadonlyArray + ): void; + const noIssues = omitInput({ foo: 3, bar: 3 }, { bar: 3 }, "foo" as "foo"); + const badCall = omitInput({ foo: 3, bar: 3 }, { foo: 3 }, "foo" as "foo"); // ERROR 2x +} + +{ + // Pick Tests + type Pick = { + [key in Keys]: O[key], + }; + + type P = { + foo: number, + bar: number, + baz: number, + }; + + declare const onlyFoo: Pick; + onlyFoo.bar; // ERROR! + onlyFoo as { foo: number }; // OK! + + declare const fooAndBaz: Pick; + fooAndBaz as { foo: number, baz: number }; // OK! + + declare function pick( + o: O, + ...keys: ReadonlyArray + ): Pick; + const picked = pick({ foo: 3, bar: 3 }, "foo"); + picked as { foo: number }; + + declare function pickInput( + o: O, + x: Pick, + ...keys: ReadonlyArray + ): void; + + const noIssues = pickInput({ foo: 3, bar: 3 }, { foo: 3 }, "foo"); + const badCall = pickInput({ foo: 3, bar: 3 }, { bar: 3 }, "foo"); // ERROR 2x +} + +================================================================================ +`; + +exports[`promises.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +declare function promiseAllByKey(o: O): Promise<{[K in keyof O]: O[K] extends Promise ? V : O[K]}>; + +promiseAllByKey({ + foo: Promise.resolve(0), + bar: 'bar' as const, +}).then(o => { + o.foo as string; // error, number ~> string + o.bar as 'bar'; // ok +}); + +=====================================output===================================== +declare function promiseAllByKey( + o: O, +): Promise<{ [K in keyof O]: O[K] extends Promise ? V : O[K] }>; + +promiseAllByKey({ + foo: Promise.resolve(0), + bar: "bar" as const, +}).then((o) => { + o.foo as string; // error, number ~> string + o.bar as "bar"; // ok +}); + +================================================================================ +`; + +exports[`reasons.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +// We make a SingeltonStringT to implement mapped types via substitution. +// Let's make sure they have good reasons for error messages. + + +// Make sure the substituted SingletonStringTs have good reasons +{ + type TakesFoo = T; + + type FooBarObj = {foo: number, bar: number}; + type MappedFooBarObj = {[key in keyof FooBarObj]: TakesFoo}; // ERROR + + ({foo: 'foo', bar: 'bar'} as MappedFooBarObj); +} + +=====================================output===================================== +// We make a SingeltonStringT to implement mapped types via substitution. +// Let's make sure they have good reasons for error messages. + +// Make sure the substituted SingletonStringTs have good reasons +{ + type TakesFoo = T; + + type FooBarObj = { foo: number, bar: number }; + type MappedFooBarObj = { [key in keyof FooBarObj]: TakesFoo }; // ERROR + + ({ foo: "foo", bar: "bar" }) as MappedFooBarObj; +} + +================================================================================ +`; + +exports[`semi_homomorphic.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type Semi = { + [key in Keys]: O[key], +} +{ + // Regular properties work + type O = {readonly foo: number}; + + type Mapped = Semi; + + declare const a: Mapped; + a.foo = 3; // ERROR + + type MappedBad = Semi; // ERROR HERE + declare const b: MappedBad; + b as {bar: empty}; // NO ERROR +} + +{ + // Indexers work + type P = {foo: number, bar: string, [string]: boolean}; + + type MappedWithIndexer = Semi; + + declare const withIndexer: MappedWithIndexer; + withIndexer as {foo: number, [string]: boolean}; // OK + withIndexer as {foo: number}; // ERROR, missing indexer + + type MappedWithoutIndexer = Semi; + + declare const withoutIndexer: MappedWithoutIndexer; + withoutIndexer as {foo: number, bar: string}; // OK + withoutIndexer as P; // Arguably should error, but pre-existing unsoundness + declare const p: P; + p as MappedWithoutIndexer; // ERROR + + // Incompatible indexers cause an error at the instantiation site + // but persist the indexer type. + type IndexedBad = Semi; // ERROR + declare const bad: IndexedBad; + bad as {[number]: boolean}; // NO ERROR + + // Indexers when the original type has none cause an error at the instantiation + // site and an any-typed indexer + type NoIndexerBad = Semi<{}, number>; // ERROR + declare const noIndexerBad: NoIndexerBad; + noIndexerBad as {[number]: empty}; // NO ERROR +} + +=====================================output===================================== +type Semi = { + [key in Keys]: O[key], +}; +{ + // Regular properties work + type O = { readonly foo: number }; + + type Mapped = Semi; + + declare const a: Mapped; + a.foo = 3; // ERROR + + type MappedBad = Semi; // ERROR HERE + declare const b: MappedBad; + b as { bar: empty }; // NO ERROR +} + +{ + // Indexers work + type P = { foo: number, bar: string, [string]: boolean }; + + type MappedWithIndexer = Semi; + + declare const withIndexer: MappedWithIndexer; + withIndexer as { foo: number, [string]: boolean }; // OK + withIndexer as { foo: number }; // ERROR, missing indexer + + type MappedWithoutIndexer = Semi; + + declare const withoutIndexer: MappedWithoutIndexer; + withoutIndexer as { foo: number, bar: string }; // OK + withoutIndexer as P; // Arguably should error, but pre-existing unsoundness + declare const p: P; + p as MappedWithoutIndexer; // ERROR + + // Incompatible indexers cause an error at the instantiation site + // but persist the indexer type. + type IndexedBad = Semi; // ERROR + declare const bad: IndexedBad; + bad as { [number]: boolean }; // NO ERROR + + // Indexers when the original type has none cause an error at the instantiation + // site and an any-typed indexer + type NoIndexerBad = Semi<{}, number>; // ERROR + declare const noIndexerBad: NoIndexerBad; + noIndexerBad as { [number]: empty }; // NO ERROR +} + +================================================================================ +`; + +exports[`test.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type O = { foo: number } +type Arr = Array; +type ROArr = ReadonlyArray; +type Tuple = [a: number, readonly b?: string]; +type Box = {contents: T}; + +type WithIndexer = { + foo: number, + [string]: string, +}; + + +type Mapped> = { + [key in keyof O]: Box, +}; + +// MappedType ~> ObjT +{ + declare const o: Mapped; + o as {foo: {contents: number}}; // OK +} + +// MappedType ~> array & tuple +{ + declare const a: Mapped; + declare const b: Mapped; + declare const c: Mapped; + a as Array<{contents: number}>; // OK + b as ReadonlyArray<{contents: number}>; // OK + c as [a: {contents: number}, readonly b?: {contents: string | void}]; // OK + a as empty; // ERROR + b as empty; // ERROR + c as empty; // ERROR +} + +// ObjT ~> MappedType +{ + declare const o: {foo: {contents: number}}; + o as Mapped; // OK + + declare const badKey: {bar: {contents: number}}; + badKey as Mapped; // ERROR + + declare const badVal: {foo: {contents: string}}; + badVal as Mapped; // ERROR +} + +// array & tuple ~> MappedType +{ + declare const a: Array<{contents: number}>; + declare const b: ReadonlyArray<{contents: number}>; + declare const c: [a: {contents: number}, readonly b?: {contents: string | void}]; + declare const badA: Array<{contents: string}>; + declare const badB: ReadonlyArray<{contents: string}>; + declare const badC: [a: {contents: number}, readonly b: {contents: string}]; + a as Mapped; // OK + b as Mapped; // OK + c as Mapped; // OK + badA as Mapped; // ERROR + badB as Mapped; // ERROR + badC as Mapped; // ERROR +} + +// No mapped types in declared classes or interfaces +{ + declare class DeclaredClass { + [x in keyof O]: number; // ERROR + } + interface I { + [x in keyof O]: number; // ERROR + } +} + +// Mapped types with indexers +{ + type MappedIndexer = Mapped; + declare const mappedIndexer: MappedIndexer; + + mappedIndexer.foo as number; // ERROR + mappedIndexer.bar as string; // ERROR + mappedIndexer as {foo: Box, [string]: Box}; // OK + + declare const indexer: {foo: Box, [string]: Box}; + indexer as MappedIndexer; // OK +} + +// Variance +{ + type ReadOnly = {readonly [key in keyof T]: T[key]}; + declare const readonly: ReadOnly; + readonly.foo as number; // OK + readonly.foo = 3; // ERROR + + type WriteOnly = {writeonly [key in keyof T]: T[key]}; + declare const writeonly: WriteOnly; + writeonly.foo as number; // ERROR + writeonly.foo = 3; // OK + + type ReadOnlyIndexer = ReadOnly; + declare const readonlyIndexer: ReadOnlyIndexer; + readonlyIndexer.qux as string; // OK + readonlyIndexer.qux = 'str'; // ERROR + + type _Unsupported = {readonly [key in keyof Arr]: Arr[key]}; // error: unsupported variance +} + +// Optionality +{ + type Partial> = {[key in keyof T]?: T[key]}; + declare const partial: Partial; + partial.foo as number; // ERROR + partial.foo as number | void; // OK + + declare const partialIndexer: Partial; + partialIndexer.qux as string; // ERROR + partialIndexer.qux as string | void; // OK + + declare const partialArr: Partial; + partialArr[0] as number; // ERROR; + declare const partialTuple: Partial; + partialTuple[0] as number; // ERROR; +} + +// Error positioning +{ + type ConstrainedBox = Box; + type MappedConstrained = { + [key in keyof O]: ConstrainedBox, + }; + + declare const constrained: MappedConstrained; // SHOULD ERROR HERE (constraint \`T extends string\` violated by O[foo] = number), NOT IN DEFINITION OF MAPPEDCONSTRAINED — but Flow currently delays the report to the cast below + constrained as {foo: {contents: number}}; // intended OK; currently ERROR — Flow surfaces the constraint violation here instead of at the type instantiation above +} + +// Error positioning +{ + type UnconstrainedKey = {[key in T]: number}; + type BadKeys = UnconstrainedKey; // ERROR HERE, NOT LINE ABOVE + declare const badKeys: BadKeys; + badKeys as empty; // ERROR +} + +=====================================output===================================== +type O = { foo: number }; +type Arr = Array; +type ROArr = ReadonlyArray; +type Tuple = [a: number, readonly b?: string]; +type Box = { contents: T }; + +type WithIndexer = { + foo: number, + [string]: string, +}; + +type Mapped> = { + [key in keyof O]: Box, +}; + +// MappedType ~> ObjT +{ + declare const o: Mapped; + o as { foo: { contents: number } }; // OK +} + +// MappedType ~> array & tuple +{ + declare const a: Mapped; + declare const b: Mapped; + declare const c: Mapped; + a as Array<{ contents: number }>; // OK + b as ReadonlyArray<{ contents: number }>; // OK + c as [a: { contents: number }, readonly b?: { contents: string | void }]; // OK + a as empty; // ERROR + b as empty; // ERROR + c as empty; // ERROR +} + +// ObjT ~> MappedType +{ + declare const o: { foo: { contents: number } }; + o as Mapped; // OK + + declare const badKey: { bar: { contents: number } }; + badKey as Mapped; // ERROR + + declare const badVal: { foo: { contents: string } }; + badVal as Mapped; // ERROR +} + +// array & tuple ~> MappedType +{ + declare const a: Array<{ contents: number }>; + declare const b: ReadonlyArray<{ contents: number }>; + declare const c: [ + a: { contents: number }, + readonly b?: { contents: string | void }, + ]; + declare const badA: Array<{ contents: string }>; + declare const badB: ReadonlyArray<{ contents: string }>; + declare const badC: [ + a: { contents: number }, + readonly b: { contents: string }, + ]; + a as Mapped; // OK + b as Mapped; // OK + c as Mapped; // OK + badA as Mapped; // ERROR + badB as Mapped; // ERROR + badC as Mapped; // ERROR +} + +// No mapped types in declared classes or interfaces +{ + declare class DeclaredClass { + [x in keyof O]: number; // ERROR + } + interface I { + [x in keyof O]: number; // ERROR + } +} + +// Mapped types with indexers +{ + type MappedIndexer = Mapped; + declare const mappedIndexer: MappedIndexer; + + mappedIndexer.foo as number; // ERROR + mappedIndexer.bar as string; // ERROR + mappedIndexer as { foo: Box, [string]: Box }; // OK + + declare const indexer: { foo: Box, [string]: Box }; + indexer as MappedIndexer; // OK +} + +// Variance +{ + type ReadOnly = { readonly [key in keyof T]: T[key] }; + declare const readonly: ReadOnly; + readonly.foo as number; // OK + readonly.foo = 3; // ERROR + + type WriteOnly = { writeonly [key in keyof T]: T[key] }; + declare const writeonly: WriteOnly; + writeonly.foo as number; // ERROR + writeonly.foo = 3; // OK + + type ReadOnlyIndexer = ReadOnly; + declare const readonlyIndexer: ReadOnlyIndexer; + readonlyIndexer.qux as string; // OK + readonlyIndexer.qux = "str"; // ERROR + + type _Unsupported = { readonly [key in keyof Arr]: Arr[key] }; // error: unsupported variance +} + +// Optionality +{ + type Partial> = { + [key in keyof T]?: T[key], + }; + declare const partial: Partial; + partial.foo as number; // ERROR + partial.foo as number | void; // OK + + declare const partialIndexer: Partial; + partialIndexer.qux as string; // ERROR + partialIndexer.qux as string | void; // OK + + declare const partialArr: Partial; + partialArr[0] as number; // ERROR; + declare const partialTuple: Partial; + partialTuple[0] as number; // ERROR; +} + +// Error positioning +{ + type ConstrainedBox = Box; + type MappedConstrained = { + [key in keyof O]: ConstrainedBox, + }; + + declare const constrained: MappedConstrained; // SHOULD ERROR HERE (constraint \`T extends string\` violated by O[foo] = number), NOT IN DEFINITION OF MAPPEDCONSTRAINED — but Flow currently delays the report to the cast below + constrained as { foo: { contents: number } }; // intended OK; currently ERROR — Flow surfaces the constraint violation here instead of at the type instantiation above +} + +// Error positioning +{ + type UnconstrainedKey = { [key in T]: number }; + type BadKeys = UnconstrainedKey; // ERROR HERE, NOT LINE ABOVE + declare const badKeys: BadKeys; + badKeys as empty; // ERROR +} + +================================================================================ +`; + +exports[`union.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] + printWidth: 80 (default) | +=====================================input====================================== +type T = {p:string}|{p:number}; +type IdentityMap = {[K in keyof O]: O[K]}; +type U = IdentityMap; +({p:0} as T); // ok +({p:0} as U); // ok +({p:'0'} as T); // ok +({p:'0'} as U); // ok +({p:false} as T); // error +({p:false} as U); // error + +=====================================output===================================== +type T = { p: string } | { p: number }; +type IdentityMap = { [K in keyof O]: O[K] }; +type U = IdentityMap; +({ p: 0 }) as T; // ok +({ p: 0 }) as U; // ok +({ p: "0" }) as T; // ok +({ p: "0" }) as U; // ok +({ p: false }) as T; // error +({ p: false }) as U; // error + +================================================================================ +`; diff --git a/tests/format/flow/flow-repo/mapped_types/aliases.js b/tests/format/flow/flow-repo/mapped_types/aliases.js new file mode 100644 index 000000000..8ff1256fb --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/aliases.js @@ -0,0 +1,16 @@ +// Mapped Type aliases should not participate in the buggy TypeAppT ~> TypeAppT special case +type ReadOnly = {readonly [key in keyof O]: O[key]}; +type ReadOnlyDeeper = ReadOnly; +type ReadOnlyDeepest = ReadOnlyDeeper; + +type O1 = {foo: number}; +type O2 = {foo: string | number}; + +declare const x: ReadOnly; +x as ReadOnly; // OK + +declare const y: ReadOnlyDeeper; +y as ReadOnlyDeeper; // OK + +declare const z: ReadOnlyDeepest; +z as ReadOnlyDeepest; // OK diff --git a/tests/format/flow/flow-repo/mapped_types/deep_types.js b/tests/format/flow/flow-repo/mapped_types/deep_types.js new file mode 100644 index 000000000..aebebd6d7 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/deep_types.js @@ -0,0 +1,57 @@ +type DeepPartialState = + // Leave primitives alone + T extends ?(number | string | boolean) + ? T + : Readonly<{ + [K in keyof T]: DeepPartialState[K]>, + }> + +{ + declare const a: DeepPartialState; + a as number; // ok + a as empty; // error: number ~> empty + + declare const b: DeepPartialState<{foo: {bar: {baz: string}}}>; + b as {readonly foo?: {readonly bar?: {readonly baz?: string}}}; + b as {readonly foo: {readonly bar?: {readonly baz?: string}}}; // error: foo not optional + b as {readonly foo?: {readonly bar: {readonly baz?: string}}}; // error: bar not optional + b as {readonly foo?: {readonly bar?: {readonly baz: string}}}; // error: baz not optional +} + +type DeepReadOnly = + T extends ReadonlyArray ? ReadonlyArray> : + T extends {...} ? {readonly [K in keyof T]: DeepReadOnly} : T; + +{ + const obj: DeepReadOnly<{ + arr: Array<{value: string, ...}>, + nested: { + value: string, + doubleNested: {value: string, ...}, + ... + }, + ... + }> = { + arr: [{value: 'a'}, {value: 'b'}], + nested: { + value: 'nested value', + doubleNested: { + value: 'double nested value', + }, + }, + value: 'top value', + }; + obj as empty; // error + + obj.arr.push({value: 'updated value'}); // error: Cannot mutate arrays. + obj.arr[0].value = 'updated value'; // error: Cannot mutate objects in arrays. + obj.arr = []; // error: Cannot change top-level values. + obj.nested = obj.nested; // error: Cannot change top-level values. + obj.nested.value = 'updated value'; // error: Cannot change nested values. + obj.nested.doubleNested = obj.nested.doubleNested; // error: Cannot change nested values. + obj.nested.doubleNested.value = 'updated value'; // error: Cannot change nested values. + + const arr: DeepReadOnly> = ['hello', 'world']; + arr.push('goodbye'); // error: Cannot mutate arrays. + arr[0] = 'goodbye'; // error: Cannot mutate values in arrays. +} diff --git a/tests/format/flow/flow-repo/mapped_types/distributivity.js b/tests/format/flow/flow-repo/mapped_types/distributivity.js new file mode 100644 index 000000000..ccfc16869 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/distributivity.js @@ -0,0 +1,30 @@ +// Tests that we correctly distribute mapped types over unions. +// A failure here would be trying to access a key from a branch +// that only exists in one branch of the union. +{ + type Homomorphic = {[key in keyof O]: O[key]}; + declare const o: Homomorphic<{foo: number} | {bar: number}>; // OK + o as {foo: number} | {bar: number}; // OK + o as {foo: empty} | {bar: empty}; // ERROR x2 +} + +{ + type SemiHomomorphic = {[key in Keys]: O[key]}; + declare const o: SemiHomomorphic<{foo: number, bar: number} | {foo: string, baz: number}, 'foo'>; + o as {foo: number} | {foo: string}; // OK + o as {}; // ERROR +} + +{ + type DistributeNullAndVoid = {writeonly [key in keyof O]: number}; + + type O = {foo: number}; + + declare const oExplicit: DistributeNullAndVoid; + oExplicit as {writeonly foo: number} | null | void; + oExplicit as {writeonly foo: number}; // ERROR 2x + + declare const oMaybe: DistributeNullAndVoid; + oMaybe as {writeonly foo: number} | null | void; + oMaybe as {writeonly foo: number}; // ERROR +} diff --git a/tests/format/flow/flow-repo/mapped_types/export.js b/tests/format/flow/flow-repo/mapped_types/export.js new file mode 100644 index 000000000..611d21f91 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/export.js @@ -0,0 +1,13 @@ +type O = {foo: number, bar?: string, readonly baz: boolean}; + +export type MappedO = {[key in keyof O]: O[key]}; +export type AddOptional = {[key in keyof O]?: O[key]}; +export type AllReadonly = {readonly [key in keyof O]: O[key]}; + +export type ParameterizedId = {[key in keyof O]: O[key]}; +export type ParameterizedPartial = {[key in keyof O]?: O[key]}; +export type ParameterizedReadonly = {readonly [key in keyof O]: O[key]}; + +export type MappedNonHomomorphic = {[key in 'foo' | 'bar']: number}; + +export type SemiHomomorphic = {[key in Keys]: O[key]}; diff --git a/tests/format/flow/flow-repo/mapped_types/format.test.js b/tests/format/flow/flow-repo/mapped_types/format.test.js new file mode 100644 index 000000000..55c339883 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["flow"]); diff --git a/tests/format/flow/flow-repo/mapped_types/identity.js b/tests/format/flow/flow-repo/mapped_types/identity.js new file mode 100644 index 000000000..8614347c7 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/identity.js @@ -0,0 +1,52 @@ +declare const any: any; + +type IdentityMap = {[K in keyof O]: O[K]}; +type Foo = IdentityMap<{ a: number, b: number }>; +declare const foo: Foo; + +({ + a: 42, // OK + b: 42, // OK +} as Foo); + +({ + a: 'asd', // Error: string ~> number + b: 'asd', // Error: string ~> number +} as Foo); + +((any as { + a: string, // Error: string ~> number + b: string, // Error: string ~> number +}) as Foo); + +({} as {} as Foo); // Error: `a` and `b` are not defined. + +any as {} as Foo; // Error: `a` and `b` are not defined. + +(foo as { + a: number, // OK + b: number, // OK +}); + +(foo as { + a: string, // Error: number ~> string + b: string, // Error: number ~> string +}); + +foo.a as empty; // Error: number ~> empty + +({ + foo: 'asd', // OK +} as IdentityMap<{ foo: number | string }>); + +({ + foo: 'asd', // OK +} as IdentityMap<{ foo: number } | { foo: string }>); + +({ + foo: true, // Error: boolean ~> number | string +} as IdentityMap<{ foo: number | string }>); + +({ + foo: true, // Error: boolean ~> number | string +} as IdentityMap<{ foo: number } | { foo: string }>); diff --git a/tests/format/flow/flow-repo/mapped_types/idx_replacement.js b/tests/format/flow/flow-repo/mapped_types/idx_replacement.js new file mode 100644 index 000000000..598b4946c --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/idx_replacement.js @@ -0,0 +1,94 @@ +declare opaque type DeepRequiredArray: ReadonlyArray< + DeepRequired>, +>; + +declare opaque type DeepRequiredObject: Required<{ + readonly [K in keyof T]: DeepRequired, +}>; + +declare opaque type IdxNonMaybeType: NonNullable; + +declare opaque type DeepRequired: T extends empty + ? $FlowFixMe // If something can pass empty, it's already unsafe + : T extends ReadonlyArray + ? DeepRequiredArray + : T extends interface {} + ? DeepRequiredObject + : IdxNonMaybeType; + +type UnboxDeepRequired = T extends DeepRequired ? V : T; + +declare function idx( + prop: T1, + accessor: (prop: NonNullable>) => T2, +): ?UnboxDeepRequired; + +// Migrated from original idx test +{ + // Objects + declare const obj1: {a: ?{b: {c: number}}}; + obj1.a.b.c; // error + idx(obj1, obj => obj.a.b.c) as ?number; // ok + idx(obj1, obj => obj["a"].b.c) as ?number; // ok + idx(obj1, obj => obj.a.b.c) as number; // error: result must be MaybeT + idx(obj1, obj => obj.a.b.c) as ?string; // error: number ~> string + idx(obj1, obj => obj["a"].b.c) as number; // error: result must be MaybeT + idx(obj1, obj => obj.notAProp); // error: prop-missing + idx(obj1, obj => obj.a = null); // error: invalid-idx + declare const obj2: {a?: {b: {c: number}}}; + idx(obj2, obj => obj.a.b.c) as ?number; // ok + idx(obj2, obj => obj.a.b.c) as number; // error: result must be MaybeT + declare const obj3: {a: null | {b: {c: number}}}; + idx(obj3, obj => obj.a.b.c) as ?number; // ok + idx(obj3, obj => obj.a.b.c) as number; // error: result must be MaybeT + // Nested maybes/optionals should get unwrapped + declare const obj4: {a?: ?(?{b: number})}; + idx(obj4, obj => obj.a.b) as ?number; // ok + + // Unions + declare const ab: {a:string}|{b:number}; + idx(ab, _ => _.a) as empty; // error + idx(ab, _ => _.b) as empty; // error + idx(ab, _ => _.c) as empty; // error + + // Classes + class Foo1 { a: ?Foo1; b: ?number; } + class Foo2 { a: Foo2 | void; b: ?number; } + class Foo3 { a: Foo3 | null; b: ?number; } + idx(new Foo1(), o => o.a.b) as ?number; // ok + idx(new Foo1(), o => o.a.b) as number; // error: result must be MaybeT + idx(new Foo1(), o => o.a = null); // error: not writable + + // Arrays + declare const arr1: Array>; + declare const arr2: Array | void>; + declare const arr3: Array | null>; + idx(arr1, arr => arr[0][0]) as ?number; // ok + idx(arr2, arr => arr[0][0]) as ?number; // ok + idx(arr3, arr => arr[0][0]) as ?number; // ok + + // Non-objects + idx(42, n => n) as ?number; // ok + idx(42, n => n) as number; // error: result must be MaybeT + idx(42, n => n.nope); // error: prop-missing + + // Weird edge cases + // Using an annotation obscures the type wrapper mechanism that idx() uses + // around the parameter it passes to the callback + idx({}, (obj: any) => obj.a.b.c) as ?number; // ok + // Can't do anything with the callback parameter other than get elements and properties off of it + idx({}, obj => obj()); // error +} + +// Additional tests +{ + declare const props: { data: { readonly id: ?string } }; + declare function nullthrows(x: ?T): T; + + const a = nullthrows(idx(props, _ => _.data.id)); + a as empty; // error + if (a != null) { + a as empty; // error + a as string; // ok + } +} diff --git a/tests/format/flow/flow-repo/mapped_types/import.js b/tests/format/flow/flow-repo/mapped_types/import.js new file mode 100644 index 000000000..1e4b02dd7 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/import.js @@ -0,0 +1,90 @@ +import type { + MappedO, + AddOptional, + AllReadonly, + ParameterizedId, + ParameterizedPartial, + ParameterizedReadonly, + MappedNonHomomorphic, + SemiHomomorphic, +} from './export'; + +// No modifiers concrete +{ + ({foo: 3, bar: 'str', baz: true} as MappedO); // OK + + declare const mapped: MappedO; + mapped as {foo: number, bar?: string, readonly baz: boolean}; // OK + mapped as {foo: empty, bar: empty, readonly baz: empty}; // ERROR + mapped.baz = true; // ERROR +} + +// Add optional concrete +{ + ({foo: undefined, bar: undefined, baz: undefined} as AddOptional); // OK + declare const addOptional: AddOptional; + addOptional as {foo?: number, bar?: string, readonly baz?: boolean}; // OK + addOptional.baz = true; // ERROR +} + +// Add readonly concrete +{ + ({foo: 3, bar: undefined, baz: true} as AllReadonly); + declare const readonly: AllReadonly; + readonly.foo = 4; // ERROR; + readonly.bar = 'str'; // ERROR; + readonly.baz = false; // ERROR; +} + +// All of these tests are the same as above but use a parameterize type alias +type O = {foo: number, bar?: string, readonly baz: boolean}; +// No modifiers parameterized +{ + ({foo: 3, bar: 'str', baz: true} as ParameterizedId); // OK + + declare const mapped: ParameterizedId; + mapped as {foo: number, bar?: string, readonly baz: boolean}; // OK + mapped as {foo: empty, bar: empty, readonly baz: empty}; // ERROR + mapped.baz = true; // ERROR +} + +// Add optional parameterized +{ + ({foo: undefined, bar: undefined, baz: undefined} as ParameterizedPartial); // OK + declare const addOptional: ParameterizedPartial; + addOptional as {foo?: number, bar?: string, readonly baz?: boolean}; // OK + addOptional.baz = true; // ERROR +} + +// Add readonly parameterized +{ + ({foo: 3, bar: undefined, baz: true} as ParameterizedReadonly); + declare const readonly: ParameterizedReadonly; + readonly.foo = 4; // ERROR; + readonly.bar = 'str'; // ERROR; + readonly.baz = false; // ERROR; +} + +// Non-homomorphic mapped types +{ + ({foo: 3, bar: 3} as MappedNonHomomorphic); // OK! + ({foo: null, bar: null} as MappedNonHomomorphic); // ERROR! +} + +// Semi-homomorphic mapped types +{ + declare const semi: SemiHomomorphic<{readonly foo: number, bar: string}, 'foo'>; + semi as {readonly foo: number}; // OK + semi as {foo: number}; // ERROR +} + +// Both homomorphic and semi-homomorphic mapped types are distributive +{ + declare const semi: SemiHomomorphic<{readonly foo: number} | {readonly foo: string}, 'foo'>; + semi as {readonly foo: number} | {readonly foo: string}; // OK! + semi as {foo: number} | {foo: string}; // ERROR 2x + + declare const homomorphic: ParameterizedReadonly<{foo: number} | {bar:number} >; + homomorphic as {readonly foo: number} | {readonly bar: number}; // OK! + homomorphic as {foo: number} | {bar: number}; // ERROR 2x +} diff --git a/tests/format/flow/flow-repo/mapped_types/instancet.js b/tests/format/flow/flow-repo/mapped_types/instancet.js new file mode 100644 index 000000000..bd03dfd29 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/instancet.js @@ -0,0 +1,42 @@ +interface I { + foo: number; +} + +type MappedInterface = {readonly [key in keyof I]: I[key]}; +{ + declare const i: MappedInterface; + i as interface {readonly foo: number}; // OK + i as interface {foo: number}; // ERROR + i as {readonly foo: number, ...}; // ERROR (class object subtyping) +} + +interface WithIndexer { + foo: number; + [string]: boolean; +} +type MappedInterfaceWithIndexer = {readonly [key in keyof WithIndexer]: WithIndexer[key]}; +{ + declare const i: MappedInterfaceWithIndexer; + i as interface {readonly foo: number, readonly [string]: boolean}; // OK + i as interface {readonly foo: number, [string]: boolean}; // ERROR +} + +class A { + static bar: number; + foo: number; +} + +type MappedInstance = {readonly [key in keyof A]: A[key]}; +{ + declare const inst: MappedInstance; + inst as interface {readonly foo: number}; // OK + inst as interface {foo: number}; // ERROR + (inst['bar']); // ERROR +} + +type MappedClass = {readonly [key in keyof Class]: Class[key]}; +{ + declare const c: MappedClass; + c as interface {readonly bar: number}; // OK + c as interface {bar: number}; // ERROR +} diff --git a/tests/format/flow/flow-repo/mapped_types/issue-2674.js b/tests/format/flow/flow-repo/mapped_types/issue-2674.js new file mode 100644 index 000000000..95f418552 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/issue-2674.js @@ -0,0 +1,7 @@ +type Map = {[K in keyof O]: 'FOO'}; +type B = Map<{ FOO: null }>; +declare const b: B; + +b.FOO as 'FOO'; // ok +b.FOO as 'BAR'; // error +b.FOO = 'BAR'; // error diff --git a/tests/format/flow/flow-repo/mapped_types/key_mirror.js b/tests/format/flow/flow-repo/mapped_types/key_mirror.js new file mode 100644 index 000000000..ada8e59bb --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/key_mirror.js @@ -0,0 +1,9 @@ +declare function keyMirror(o: O): {[K in keyof O]: K}; + +var o = keyMirror({ + FOO: null, + BAR: null, +}); + +o.FOO as 'FOO'; // ok +o.FOO as 'BAR'; // error, 'FOO' incompatible with 'BAR' diff --git a/tests/format/flow/flow-repo/mapped_types/lti_implicit_instantiation.js b/tests/format/flow/flow-repo/mapped_types/lti_implicit_instantiation.js new file mode 100644 index 000000000..d317c13e5 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/lti_implicit_instantiation.js @@ -0,0 +1,9 @@ +type F = Readonly<{ + log: (() => T) => void, + ... + }>; + + declare function map(o1: O): {[K in keyof O]: O[K] extends F ? V : empty}; + + const foo = Object.freeze({bar: {log: (f: () => string) => {}}}) + const o1 = map(foo) as {readonly bar: number}; // error diff --git a/tests/format/flow/flow-repo/mapped_types/minus_optional.js b/tests/format/flow/flow-repo/mapped_types/minus_optional.js new file mode 100644 index 000000000..3847cfcf3 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/minus_optional.js @@ -0,0 +1,67 @@ +type O = {foo: number, bar?: string}; + +// -? removes optionality from optional props +type Required1 = {[K in keyof O]-?: O[K]}; +declare const r1: Required1; +r1 as {foo: number, bar: string}; // OK +({foo: 1}) as Required1; // ERROR - bar is required + +// -? on already-required props is a no-op +type AllRequired = {foo: number, bar: string}; +type Same = {[K in keyof AllRequired]-?: AllRequired[K]}; +declare const s: Same; +s as AllRequired; // OK + +// Mimic Required utility +type MyRequired = {[K in keyof T]-?: T[K]}; +type X = MyRequired<{a?: number, b?: string, c: boolean}>; +declare const x: X; +x as {a: number, b: string, c: boolean}; // OK +({a: 1, c: true}) as X; // ERROR - b is required + +// Tuple input: -? clears per-element optional bit +type T = [a: string, b?: number, c?: boolean]; +type U = {[K in keyof T]-?: T[K]}; +declare const u: U; +u as [string, number, boolean]; // OK +(['']) as U; // ERROR - elements 1 and 2 are required + +// Nested: -? then +? roundtrip +type RoundTrip = {[K in keyof MyRequired]+?: MyRequired[K]}; +type Y = RoundTrip<{a?: number}>; +declare const y: Y; +y as {a?: number}; // OK + +// Required-prop with explicit `void` in the value type: -? does NOT strip void, +// since the source property was not optional. Matches TS. +type ReqWithVoid = {a: string | void}; +type ReqWithVoidR = MyRequired; +declare const rwv: ReqWithVoidR; +rwv.a as string | void; // OK +rwv.a as string; // ERROR - void survives because source prop was not optional + +// Required tuple element with `void` in the value type: -? does NOT strip void. +type ReqTupleWithVoid = [string | void]; +type ReqTupleWithVoidR = {[K in keyof ReqTupleWithVoid]-?: ReqTupleWithVoid[K]}; +declare const rtv: ReqTupleWithVoidR; +rtv[0] as string | void; // OK +rtv[0] as string; // ERROR - void survives + +// Plain Array element with `void`: -? DOES strip void from the array element type. +// TS: R> = Array. +type ArrWithVoid = Array; +type ArrWithVoidR = {[K in keyof ArrWithVoid]-?: ArrWithVoid[K]}; +declare const arr: ArrWithVoidR; +arr as Array; // OK - undefined stripped from array element +declare const a1: Array; +a1 as ArrWithVoidR; // ERROR - void no longer accepted + +// Tuple-wide elem_t: under -? the tuple-wide element type should no longer contain +// `void` contributed by originally-optional elements. Verified via computed +// numeric access (which reads elem_t, not the per-index element list). +type TWide = [a: string, b?: number, c?: boolean]; +type UWide = {[K in keyof TWide]-?: TWide[K]}; +declare const uWide: UWide; +declare const idx: number; +uWide[idx] as string | number | boolean; // OK - void filtered from elem_t +uWide.forEach((x) => { x as string | number | boolean; }); // OK - callback param uses elem_t diff --git a/tests/format/flow/flow-repo/mapped_types/no_explicit_exactness.js b/tests/format/flow/flow-repo/mapped_types/no_explicit_exactness.js new file mode 100644 index 000000000..deccd99c8 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/no_explicit_exactness.js @@ -0,0 +1,14 @@ +// Homomorphic mapped types cannot be explicitly exact or inexact. They take on +// the exactness of their input type + + +type O = {foo: number}; + +type MappedExact = {| // ERROR + [key in keyof O]: number +|} + +type MappedInexact = { // ERROR + [key in keyof O]: number, + ... +} diff --git a/tests/format/flow/flow-repo/mapped_types/no_other_props.js b/tests/format/flow/flow-repo/mapped_types/no_other_props.js new file mode 100644 index 000000000..940b4798a --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/no_other_props.js @@ -0,0 +1,25 @@ +// Mapped types cannot have extra properties yet + +type MappedFirst = { + [key in keyof {foo: number}]: number, // ERROR + prop: number, +}; +type SliceFirst = { + prop: number, + [key in keyof {foo: number}]: number, // ERROR +}; +type SpreadFirst = { + ...{prop: number}, + [key in keyof {foo: number}]: number, // ERROR +}; +type MappedLater1 = { + ...{prop: number}, + foo: number, + [key in keyof {foo: number}]: number, // ERROR +}; +type MappedLater2 = { + foo: number, + ...{prop: number}, + foo: number, + [key in keyof {foo: number}]: number, // ERROR +}; diff --git a/tests/format/flow/flow-repo/mapped_types/non_homomorphic.js b/tests/format/flow/flow-repo/mapped_types/non_homomorphic.js new file mode 100644 index 000000000..d504026b1 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/non_homomorphic.js @@ -0,0 +1,50 @@ +{ + // Basic functionality test + type Keys = 'foo' | 'bar'; + type Mapped = {[key in Keys]: number}; + + declare const mapped: Mapped; + mapped.foo as number; // OK! + mapped.foo = 3; // OK! + + + mapped as empty; // ERROR! + mapped as {foo: empty, bar: empty}; // ERROR 2x + mapped as {foo: number, bar: number}; // OK! +} + +{ + // Leftover types are converted to indexers + type WithLeftovers = {[key in 'foo' | number]: number}; + declare const withLeftovers: WithLeftovers; + withLeftovers as {[number]: number, foo: number}; // OK! + withLeftovers as {[number]: string, foo: string}; // ERROR 2x +} + +{ + // Key types must be string | number | symbol, otherwise error on the + // definition. + // TS Reference: https://www.typescriptlang.org/play?ts=5.0.4#code/C4TwDgpgBAsghmSATKBeKBvA2gawiKASwDsoAjAewoBsI5iBdALimIFcBbMiAJwF8A3EA + type BadKeys = {[key in boolean]: number}; // ERROR! + declare const badKeys: BadKeys; + badKeys as any; +} + +{ + // Tests using mapped types in output positions for implicit instantiation + declare function keyMirror(...x: ReadonlyArray): {[key in T]: key}; + + let keyMirroredObject = keyMirror('foo', 'bar', 'baz'); + keyMirroredObject as {foo: 'foo', bar: 'bar', baz: 'baz'}; + + keyMirroredObject as {foo: 'bar', bar: 'baz', baz: 'foo'}; // ERROR x3 +} + +{ + // Tests using mapped types in input positions for implicit instantiation + declare function ObjWithKeys(x: {[key in T]: number}, ...y: ReadonlyArray): {[key in T]: number}; + const noContext = ObjWithKeys({foo: 3, bar: 3}, 'foo', 'bar'); // OK! + noContext as {foo: number, bar: number}; // OK! + noContext as {foo: empty, bar: empty, baz: number}; // ERROR 3x + const withReturnAnnot: {foo: number, bar: number} = ObjWithKeys({foo: 3, bar: 3}, 'foo', 'bar'); // OK! +} diff --git a/tests/format/flow/flow-repo/mapped_types/non_homomorphic_resolution.js b/tests/format/flow/flow-repo/mapped_types/non_homomorphic_resolution.js new file mode 100644 index 000000000..19149cf23 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/non_homomorphic_resolution.js @@ -0,0 +1,29 @@ +// This test checks ensures that we resolve TypeAppTs, KeysT, EvalTs, etc +// before trying to construct the mapped type. + +type Mapped = {[key in T]: number}; + +{ + type TypeApp = T; + type MappedTypeApp = Mapped>; + declare const mappedTypeApp: MappedTypeApp; + mappedTypeApp as empty; // ERROR + mappedTypeApp as {foo: number}; // OK! +} + +{ + type Keys = keyof {foo: number}; + type MappedKeys = Mapped; + declare const mappedKeys: MappedKeys; + mappedKeys as empty; // ERROR + mappedKeys as {foo: number}; // OK! +} + +{ + type O = {foo: 'foo'}; + type Eval = O['foo']; + type MappedEval = Mapped; + declare const mappedEval: MappedEval; + mappedEval as empty; // ERROR + mappedEval as {foo: number}; +} diff --git a/tests/format/flow/flow-repo/mapped_types/pick_and_omit.js b/tests/format/flow/flow-repo/mapped_types/pick_and_omit.js new file mode 100644 index 000000000..359076a1f --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/pick_and_omit.js @@ -0,0 +1,84 @@ +//@flow +{ + // Omit Tests + type Exclude = T extends U ? empty : T; + + type Omit = { + [key in Exclude]: O[key], + }; + + type O = { + foo: number, + bar: number, + baz: number, + }; + + declare const noFoo: Omit; + noFoo.foo = 3; // ERROR! + noFoo as {bar: number, baz: number}; // OK! + + declare const noFooOrBar: Omit; + noFooOrBar as {baz: number}; // OK! + + declare const noKeysBad: Omit; // ERROR! string is not a subtype of keyof O + noKeysBad as {}; + + declare const noKeysGood: Omit; + noKeysGood as {}; // OK! + + declare function omit( + o: O, + ...keys: ReadonlyArray + ): Omit; + // KP: We need proper natural inference to make this work with no annotations + const noBarOrBaz = omit( + {foo: 3, bar: 3, baz: 3}, + 'bar' as 'bar', + 'baz' as 'baz', + ); + noBarOrBaz as {foo: number}; // OK! + noBarOrBaz.foo as number; // OK! + noBarOrBaz.foo = 3; // OK! + + declare function omitInput( + o: O, + x: Omit, + ...keys: ReadonlyArray + ): void; + const noIssues = omitInput({foo: 3, bar: 3}, {bar: 3}, 'foo' as 'foo'); + const badCall = omitInput({foo: 3, bar: 3}, {foo: 3}, 'foo' as 'foo'); // ERROR 2x +} + +{ + // Pick Tests + type Pick = {[key in Keys]: O[key]}; + + type P = { + foo: number, + bar: number, + baz: number, + }; + + declare const onlyFoo: Pick; + onlyFoo.bar; // ERROR! + onlyFoo as {foo: number}; // OK! + + declare const fooAndBaz: Pick; + fooAndBaz as {foo: number, baz: number}; // OK! + + declare function pick( + o: O, + ...keys: ReadonlyArray + ): Pick; + const picked = pick({foo: 3, bar: 3}, 'foo'); + picked as {foo: number}; + + declare function pickInput( + o: O, + x: Pick, + ...keys: ReadonlyArray + ): void; + + const noIssues = pickInput({foo: 3, bar: 3}, {foo: 3}, 'foo'); + const badCall = pickInput({foo: 3, bar: 3}, {bar: 3}, 'foo'); // ERROR 2x +} diff --git a/tests/format/flow/flow-repo/mapped_types/promises.js b/tests/format/flow/flow-repo/mapped_types/promises.js new file mode 100644 index 000000000..189adfb1d --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/promises.js @@ -0,0 +1,9 @@ +declare function promiseAllByKey(o: O): Promise<{[K in keyof O]: O[K] extends Promise ? V : O[K]}>; + +promiseAllByKey({ + foo: Promise.resolve(0), + bar: 'bar' as const, +}).then(o => { + o.foo as string; // error, number ~> string + o.bar as 'bar'; // ok +}); diff --git a/tests/format/flow/flow-repo/mapped_types/reasons.js b/tests/format/flow/flow-repo/mapped_types/reasons.js new file mode 100644 index 000000000..2d466e1d9 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/reasons.js @@ -0,0 +1,13 @@ +// We make a SingeltonStringT to implement mapped types via substitution. +// Let's make sure they have good reasons for error messages. + + +// Make sure the substituted SingletonStringTs have good reasons +{ + type TakesFoo = T; + + type FooBarObj = {foo: number, bar: number}; + type MappedFooBarObj = {[key in keyof FooBarObj]: TakesFoo}; // ERROR + + ({foo: 'foo', bar: 'bar'} as MappedFooBarObj); +} diff --git a/tests/format/flow/flow-repo/mapped_types/semi_homomorphic.js b/tests/format/flow/flow-repo/mapped_types/semi_homomorphic.js new file mode 100644 index 000000000..401b100c2 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/semi_homomorphic.js @@ -0,0 +1,47 @@ +type Semi = { + [key in Keys]: O[key], +} +{ + // Regular properties work + type O = {readonly foo: number}; + + type Mapped = Semi; + + declare const a: Mapped; + a.foo = 3; // ERROR + + type MappedBad = Semi; // ERROR HERE + declare const b: MappedBad; + b as {bar: empty}; // NO ERROR +} + +{ + // Indexers work + type P = {foo: number, bar: string, [string]: boolean}; + + type MappedWithIndexer = Semi; + + declare const withIndexer: MappedWithIndexer; + withIndexer as {foo: number, [string]: boolean}; // OK + withIndexer as {foo: number}; // ERROR, missing indexer + + type MappedWithoutIndexer = Semi; + + declare const withoutIndexer: MappedWithoutIndexer; + withoutIndexer as {foo: number, bar: string}; // OK + withoutIndexer as P; // Arguably should error, but pre-existing unsoundness + declare const p: P; + p as MappedWithoutIndexer; // ERROR + + // Incompatible indexers cause an error at the instantiation site + // but persist the indexer type. + type IndexedBad = Semi; // ERROR + declare const bad: IndexedBad; + bad as {[number]: boolean}; // NO ERROR + + // Indexers when the original type has none cause an error at the instantiation + // site and an any-typed indexer + type NoIndexerBad = Semi<{}, number>; // ERROR + declare const noIndexerBad: NoIndexerBad; + noIndexerBad as {[number]: empty}; // NO ERROR +} diff --git a/tests/format/flow/flow-repo/mapped_types/test.js b/tests/format/flow/flow-repo/mapped_types/test.js new file mode 100644 index 000000000..d92cd2d8c --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/test.js @@ -0,0 +1,141 @@ +type O = { foo: number } +type Arr = Array; +type ROArr = ReadonlyArray; +type Tuple = [a: number, readonly b?: string]; +type Box = {contents: T}; + +type WithIndexer = { + foo: number, + [string]: string, +}; + + +type Mapped> = { + [key in keyof O]: Box, +}; + +// MappedType ~> ObjT +{ + declare const o: Mapped; + o as {foo: {contents: number}}; // OK +} + +// MappedType ~> array & tuple +{ + declare const a: Mapped; + declare const b: Mapped; + declare const c: Mapped; + a as Array<{contents: number}>; // OK + b as ReadonlyArray<{contents: number}>; // OK + c as [a: {contents: number}, readonly b?: {contents: string | void}]; // OK + a as empty; // ERROR + b as empty; // ERROR + c as empty; // ERROR +} + +// ObjT ~> MappedType +{ + declare const o: {foo: {contents: number}}; + o as Mapped; // OK + + declare const badKey: {bar: {contents: number}}; + badKey as Mapped; // ERROR + + declare const badVal: {foo: {contents: string}}; + badVal as Mapped; // ERROR +} + +// array & tuple ~> MappedType +{ + declare const a: Array<{contents: number}>; + declare const b: ReadonlyArray<{contents: number}>; + declare const c: [a: {contents: number}, readonly b?: {contents: string | void}]; + declare const badA: Array<{contents: string}>; + declare const badB: ReadonlyArray<{contents: string}>; + declare const badC: [a: {contents: number}, readonly b: {contents: string}]; + a as Mapped; // OK + b as Mapped; // OK + c as Mapped; // OK + badA as Mapped; // ERROR + badB as Mapped; // ERROR + badC as Mapped; // ERROR +} + +// No mapped types in declared classes or interfaces +{ + declare class DeclaredClass { + [x in keyof O]: number; // ERROR + } + interface I { + [x in keyof O]: number; // ERROR + } +} + +// Mapped types with indexers +{ + type MappedIndexer = Mapped; + declare const mappedIndexer: MappedIndexer; + + mappedIndexer.foo as number; // ERROR + mappedIndexer.bar as string; // ERROR + mappedIndexer as {foo: Box, [string]: Box}; // OK + + declare const indexer: {foo: Box, [string]: Box}; + indexer as MappedIndexer; // OK +} + +// Variance +{ + type ReadOnly = {readonly [key in keyof T]: T[key]}; + declare const readonly: ReadOnly; + readonly.foo as number; // OK + readonly.foo = 3; // ERROR + + type WriteOnly = {writeonly [key in keyof T]: T[key]}; + declare const writeonly: WriteOnly; + writeonly.foo as number; // ERROR + writeonly.foo = 3; // OK + + type ReadOnlyIndexer = ReadOnly; + declare const readonlyIndexer: ReadOnlyIndexer; + readonlyIndexer.qux as string; // OK + readonlyIndexer.qux = 'str'; // ERROR + + type _Unsupported = {readonly [key in keyof Arr]: Arr[key]}; // error: unsupported variance +} + +// Optionality +{ + type Partial> = {[key in keyof T]?: T[key]}; + declare const partial: Partial; + partial.foo as number; // ERROR + partial.foo as number | void; // OK + + declare const partialIndexer: Partial; + partialIndexer.qux as string; // ERROR + partialIndexer.qux as string | void; // OK + + declare const partialArr: Partial; + partialArr[0] as number; // ERROR; + declare const partialTuple: Partial; + partialTuple[0] as number; // ERROR; +} + +// Error positioning +{ + type ConstrainedBox = Box; + type MappedConstrained = { + [key in keyof O]: ConstrainedBox, + }; + + declare const constrained: MappedConstrained; // SHOULD ERROR HERE (constraint `T extends string` violated by O[foo] = number), NOT IN DEFINITION OF MAPPEDCONSTRAINED — but Flow currently delays the report to the cast below + constrained as {foo: {contents: number}}; // intended OK; currently ERROR — Flow surfaces the constraint violation here instead of at the type instantiation above +} + +// Error positioning +{ + type UnconstrainedKey = {[key in T]: number}; + type BadKeys = UnconstrainedKey; // ERROR HERE, NOT LINE ABOVE + declare const badKeys: BadKeys; + badKeys as empty; // ERROR +} diff --git a/tests/format/flow/flow-repo/mapped_types/union.js b/tests/format/flow/flow-repo/mapped_types/union.js new file mode 100644 index 000000000..f2fbe1ce4 --- /dev/null +++ b/tests/format/flow/flow-repo/mapped_types/union.js @@ -0,0 +1,9 @@ +type T = {p:string}|{p:number}; +type IdentityMap = {[K in keyof O]: O[K]}; +type U = IdentityMap; +({p:0} as T); // ok +({p:0} as U); // ok +({p:'0'} as T); // ok +({p:'0'} as U); // ok +({p:false} as T); // error +({p:false} as U); // error diff --git a/yarn.lock b/yarn.lock index b6f564923..997b46265 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4977,10 +4977,10 @@ __metadata: languageName: node linkType: hard -"flow-parser@npm:0.316.0": - version: 0.316.0 - resolution: "flow-parser@npm:0.316.0" - checksum: 10/b63d4800227cddddea731e5581563d5534874777c594fe1a9da5c8a2a67c47ea3aadb20785d2cb9b036af0fc4439682709e3d54f9fd303a23d1967d7737e1d92 +"flow-parser@npm:0.317.0": + version: 0.317.0 + resolution: "flow-parser@npm:0.317.0" + checksum: 10/2e036440863edbfe2ffcb589a4a7bc561db2b0d9808d063b0b84fe685fb3a8e6d245981222ec3be68d941435a561473bdcdb730546058a202747ea881cbfb786 languageName: node linkType: hard @@ -8134,7 +8134,7 @@ __metadata: fast-json-stable-stringify: "npm:2.1.0" file-entry-cache: "npm:11.1.3" find-cache-directory: "npm:6.0.0" - flow-parser: "npm:0.316.0" + flow-parser: "npm:0.317.0" get-east-asian-width: "npm:1.6.0" get-port: "npm:7.2.0" get-stdin: "npm:9.0.0"