diff --git a/.eslintrc b/.eslintrc index d1c7292a..d9ad961b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,7 +45,7 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-shadow": ["error"], + "@typescript-eslint/no-shadow": ["off"], "@typescript-eslint/no-use-before-define": ["error"], "@typescript-eslint/ban-types": "off", "prefer-rest-params": "off", diff --git a/.github/workflows/build-and-test-types.yml b/.github/workflows/build-and-test-types.yml index 63521db3..39b110b7 100644 --- a/.github/workflows/build-and-test-types.yml +++ b/.github/workflows/build-and-test-types.yml @@ -62,15 +62,24 @@ jobs: - name: Install deps run: yarn install - - name: Install TypeScript ${{ matrix.ts }} - run: yarn add typescript@${{ matrix.ts }} - + # Build with the actual TS version in the repo - name: Pack run: yarn build && yarn pack - name: Install build artifact run: yarn add ./package.tgz + # Then install the specific version to test against + - name: Install TypeScript ${{ matrix.ts }} + run: yarn add --dev typescript@${{ matrix.ts }} + + - name: 'Remove source to ensure packaged types are used' + run: rm -rf src + + # Remove config line that points "reselect" to the `src` folder, + # so that the typetest will use the installed version instead + - run: sed -i -e /@remap-prod-remove-line/d ./typescript_test/tsconfig.json + - name: Test types run: | ./node_modules/.bin/tsc --version diff --git a/package.json b/package.json index 55f6d0fd..214ca9da 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "scripts": { "build:commonjs": "cross-env BABEL_ENV=commonjs babel src/*.ts --ignore src/types.ts --extensions .ts --out-dir lib ", - "build:es": "babel src/*.ts --ignore src/types.ts --extensions .ts --out-dir es", + "build:es": "babel src/*.ts --ignore src/types.ts --extensions .ts --out-dir es && cp src/versionedTypes/package.dist.json es/versionedTypes/package.json", "build:umd": "cross-env NODE_ENV=development rollup -c -o dist/reselect.js", "build:umd:min": "cross-env NODE_ENV=production rollup -c -o dist/reselect.min.js", "build:types": "tsc", diff --git a/src/types.ts b/src/types.ts index 4690b810..4a016677 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,6 @@ +import type { MergeParameters } from './versionedTypes' +export type { MergeParameters } from './versionedTypes' + /* * * Reselect Data Types @@ -94,56 +97,6 @@ export type GetParamsFromSelectors< RemainingItems extends readonly unknown[] = Tail> > = RemainingItems -/** Given a set of input selectors, extracts the intersected parameters to determine - * what values can actually be passed to all of the input selectors at once - * WARNING: "you are not expected to understand this" :) - */ -export type MergeParameters< - // The actual array of input selectors - T extends readonly UnknownFunction[], - // Given those selectors, we do several transformations on the types in sequence: - // 1) Extract "the type of parameters" for each input selector, so that we now have - // a tuple of all those parameters - ParamsArrays extends readonly any[][] = ExtractParams, - // 2) Transpose the parameter tuples. - // Originally, we have nested arrays with "all params from input", "from input 2", etc: - // `[ [i1a, i1b, i1c], [i2a, i2b, i2c], [i3a, i3b, i3c] ], - // In order to intersect the params at each index, we need to transpose them so that - // we have "all the first args", "all the second args", and so on: - // `[ [i1a, i2a, i3a], [i1b, i2b, i3b], [i1c, i2c, i3c] ] - // Unfortunately, this step also turns the arrays into a union, and weirder, it is - // a union of all possible combinations for all input functions, so there's duplicates. - TransposedArrays = Transpose, - // 3) Turn the union of arrays back into a nested tuple. Order does not matter here. - TuplifiedArrays extends any[] = TuplifyUnion, - // 4) Find the longest params array out of the ones we have. - // Note that this is actually the _nested_ data we wanted out of the transpose step, - // so it has all the right pieces we need. - LongestParamsArray extends readonly any[] = LongestArray -> = - // After all that preparation work, we can actually do parameter extraction. - // These steps work somewhat inside out (jump ahead to the middle): - // 11) Finally, after all that, run a shallow expansion on the values to make the user-visible - // field details more readable when viewing the selector's type in a hover box. - ExpandItems< - // 10) Tuples can have field names attached, and it seems to work better to remove those - RemoveNames<{ - // 5) We know the longest params array has N args. Loop over the indices of that array. - // 6) For each index, do a check to ensure that we're _only_ checking numeric indices, - // not any field names for array functions like `slice()` - [index in keyof LongestParamsArray]: LongestParamsArray[index] extends LongestParamsArray[number] - ? // 9) Any object types that were intersected may have had - IgnoreInvalidIntersections< - // 8) Then, intersect all of the parameters for this arg together. - IntersectAll< - // 7) Since this is a _nested_ array, extract the right sub-array for this index - LongestParamsArray[index] - > - > - : never - }> - > - /* * * Reselect Internal Utility Types @@ -153,28 +106,11 @@ export type MergeParameters< /** Any function with arguments */ export type UnknownFunction = (...args: any[]) => any -/** An object with no fields */ -type EmptyObject = { - [K in any]: never -} - -type IgnoreInvalidIntersections = T extends EmptyObject ? never : T - -/** Extract the parameters from all functions as a tuple */ -export type ExtractParams = { - [index in keyof T]: T[index] extends T[number] ? Parameters : never -} - /** Extract the return type from all functions as a tuple */ export type ExtractReturnType = { [index in keyof T]: T[index] extends T[number] ? ReturnType : never } -/** Recursively expand all fields in an object for easier reading */ -export type ExpandItems = { - [index in keyof T]: T[index] extends T[number] ? Expand : never -} - /** First item in an array */ export type Head = T extends [any, ...any[]] ? T[0] : never /** All other items in an array */ @@ -191,58 +127,6 @@ export type List = ReadonlyArray export type Has = [U1] extends [U] ? 1 : 0 -/** Select the longer of two arrays */ -export type Longest = L extends unknown - ? L1 extends unknown - ? { 0: L1; 1: L }[Has] - : never - : never - -/** Recurse over a nested array to locate the longest one. - * Acts like a type-level `reduce()` - */ -export type LongestArray = - // If this isn't a tuple, all indices are the same, we can't tell a difference - IsTuple extends '0' - ? // so just return the type of the first item - S[0] - : // If there's two nested arrays remaining, compare them - S extends [any[], any[]] - ? Longest - : // If there's more than two, extract their types, treat the remainder as a smaller array - S extends [any[], any[], ...infer Rest] - ? // then compare those two, recurse through the smaller array, and compare vs its result - Longest< - Longest, - Rest extends any[][] ? LongestArray : [] - > - : // If there's one item left, return it - S extends [any[]] - ? S[0] - : never - -/** Recursive type for intersecting together all items in a tuple, to determine - * the final parameter type at a given argument index in the generated selector. */ -export type IntersectAll = IsTuple extends '0' - ? T[0] - : _IntersectAll - -type IfJustNullish = [T] extends [undefined | null] - ? True - : False - -/** Intersect a pair of types together, for use in parameter type calculation. - * This is made much more complex because we need to correctly handle cases - * where a function has fewer parameters and the type is `undefined`, as well as - * optional params or params that have `null` or `undefined` as part of a union. - * - * If the next type by itself is `null` or `undefined`, we exclude it and return - * the other type. Otherwise, intersect them together. - */ -type _IntersectAll = T extends [infer First, ...infer Rest] - ? _IntersectAll> - : R - /* * * External/Copied Utility Types @@ -253,32 +137,22 @@ type _IntersectAll = T extends [infer First, ...infer Rest] * Source: https://github.com/sindresorhus/type-fest/blob/main/source/union-to-intersection.d.ts * Reference: https://github.com/microsoft/TypeScript/issues/29594 */ -export type UnionToIntersection = ( +export type UnionToIntersection = // `extends unknown` is always going to be the case and is used to convert the // `Union` into a [distributive conditional // type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types). - Union extends unknown - ? // The union type is used as the only argument to a function since the union - // of function arguments is an intersection. - (distributedUnion: Union) => void - : // This won't happen. - never - // Infer the `Intersection` type since TypeScript represents the positional - // arguments of unions of functions as an intersection of the union. -) extends (mergedIntersection: infer Intersection) => void - ? Intersection - : never - -/** - * Removes field names from a tuple - * Source: https://stackoverflow.com/a/63571175/62937 - */ -type RemoveNames = [any, ...T] extends [ - any, - ...infer U -] - ? U - : never + ( + Union extends unknown + ? // The union type is used as the only argument to a function since the union + // of function arguments is an intersection. + (distributedUnion: Union) => void + : // This won't happen. + never + ) extends // Infer the `Intersection` type since TypeScript represents the positional + // arguments of unions of functions as an intersection of the union. + (mergedIntersection: infer Intersection) => void + ? Intersection + : never /** * Assorted util types for type-level conditional logic @@ -313,7 +187,7 @@ type LastOf = UnionToIntersection< : never // TS4.1+ -type TuplifyUnion< +export type TuplifyUnion< T, L = LastOf, N = [T] extends [never] ? true : false @@ -331,21 +205,6 @@ export type ObjValueTuple< ? ObjValueTuple : R -/** - * Transposes nested arrays - * Source: https://stackoverflow.com/a/66303933/62937 - */ -type Transpose = T[Extract< - keyof T, - T extends readonly any[] ? number : unknown ->] extends infer V - ? { - [K in keyof V]: { - [L in keyof T]: K extends keyof T[L] ? T[L][K] : undefined - } - } - : never - /** Utility type to infer the type of "all params of a function except the first", so we can determine what arguments a memoize function accepts */ export type DropFirst = T extends [unknown, ...infer U] ? U diff --git a/src/versionedTypes/index.ts b/src/versionedTypes/index.ts new file mode 100644 index 00000000..b2ff693e --- /dev/null +++ b/src/versionedTypes/index.ts @@ -0,0 +1 @@ +export { MergeParameters } from './ts47-mergeParameters' diff --git a/src/versionedTypes/package.dist.json b/src/versionedTypes/package.dist.json new file mode 100644 index 00000000..600d9d1e --- /dev/null +++ b/src/versionedTypes/package.dist.json @@ -0,0 +1,14 @@ +{ + "typesVersions": { + ">=4.7": { + "index": [ + "./ts47-mergeParameters.d.ts" + ] + }, + "<4.7": { + "index": [ + "./ts46-mergeParameters.d.ts" + ] + } + } +} diff --git a/src/versionedTypes/ts46-mergeParameters.ts b/src/versionedTypes/ts46-mergeParameters.ts new file mode 100644 index 00000000..bf92fbff --- /dev/null +++ b/src/versionedTypes/ts46-mergeParameters.ts @@ -0,0 +1,171 @@ +import type { + UnknownFunction, + Expand, + TuplifyUnion, + Has, + List, + IsTuple +} from '../types' + +/** Given a set of input selectors, extracts the intersected parameters to determine + * what values can actually be passed to all of the input selectors at once + * WARNING: "you are not expected to understand this" :) + */ +export type MergeParameters< + // The actual array of input selectors + T extends readonly UnknownFunction[], + // Given those selectors, we do several transformations on the types in sequence: + // 1) Extract "the type of parameters" for each input selector, so that we now have + // a tuple of all those parameters + ParamsArrays extends readonly any[][] = ExtractParams, + // 2) Transpose the parameter tuples. + // Originally, we have nested arrays with "all params from input", "from input 2", etc: + // `[ [i1a, i1b, i1c], [i2a, i2b, i2c], [i3a, i3b, i3c] ], + // In order to intersect the params at each index, we need to transpose them so that + // we have "all the first args", "all the second args", and so on: + // `[ [i1a, i2a, i3a], [i1b, i2b, i3b], [i1c, i2c, i3c] ] + // Unfortunately, this step also turns the arrays into a union, and weirder, it is + // a union of all possible combinations for all input functions, so there's duplicates. + TransposedArrays = Transpose, + // 3) Turn the union of arrays back into a nested tuple. Order does not matter here. + TuplifiedArrays extends any[] = TuplifyUnion, + // 4) Find the longest params array out of the ones we have. + // Note that this is actually the _nested_ data we wanted out of the transpose step, + // so it has all the right pieces we need. + LongestParamsArray extends readonly any[] = LongestArray +> = + // After all that preparation work, we can actually do parameter extraction. + // These steps work somewhat inside out (jump ahead to the middle): + // 11) Finally, after all that, run a shallow expansion on the values to make the user-visible + // field details more readable when viewing the selector's type in a hover box. + ExpandItems< + // 10) Tuples can have field names attached, and it seems to work better to remove those + RemoveNames<{ + // 5) We know the longest params array has N args. Loop over the indices of that array. + // 6) For each index, do a check to ensure that we're _only_ checking numeric indices, + // not any field names for array functions like `slice()` + [index in keyof LongestParamsArray]: LongestParamsArray[index] extends LongestParamsArray[number] + ? // 9) Any object types that were intersected may have had + IgnoreInvalidIntersections< + // 8) Then, intersect all of the parameters for this arg together. + IntersectAll< + // 7) Since this is a _nested_ array, extract the right sub-array for this index + LongestParamsArray[index] + > + > + : never + }> + > + +/* + * + * Reselect Internal Utility Types + * + */ + +/* + * + * Reselect Internal Utility Types + * + */ + +/** An object with no fields */ +type EmptyObject = { + [K in any]: never +} + +type IgnoreInvalidIntersections = T extends EmptyObject ? never : T + +/** Extract the parameters from all functions as a tuple */ +export type ExtractParams = { + [index in keyof T]: T[index] extends T[number] ? Parameters : never +} + +/** Recursively expand all fields in an object for easier reading */ +export type ExpandItems = { + [index in keyof T]: T[index] extends T[number] ? Expand : never +} + +/** Select the longer of two arrays */ +export type Longest = L extends unknown + ? L1 extends unknown + ? { 0: L1; 1: L }[Has] + : never + : never + +/** Recurse over a nested array to locate the longest one. + * Acts like a type-level `reduce()` + */ +export type LongestArray = + // If this isn't a tuple, all indices are the same, we can't tell a difference + IsTuple extends '0' + ? // so just return the type of the first item + S[0] + : // If there's two nested arrays remaining, compare them + S extends [any[], any[]] + ? Longest + : // If there's more than two, extract their types, treat the remainder as a smaller array + S extends [any[], any[], ...infer Rest] + ? // then compare those two, recurse through the smaller array, and compare vs its result + Longest< + Longest, + Rest extends any[][] ? LongestArray : [] + > + : // If there's one item left, return it + S extends [any[]] + ? S[0] + : never + +/** Recursive type for intersecting together all items in a tuple, to determine + * the final parameter type at a given argument index in the generated selector. */ +export type IntersectAll = IsTuple extends '0' + ? T[0] + : _IntersectAll + +type IfJustNullish = [T] extends [undefined | null] + ? True + : False + +/** Intersect a pair of types together, for use in parameter type calculation. + * This is made much more complex because we need to correctly handle cases + * where a function has fewer parameters and the type is `undefined`, as well as + * optional params or params that have `null` or `undefined` as part of a union. + * + * If the next type by itself is `null` or `undefined`, we exclude it and return + * the other type. Otherwise, intersect them together. + */ +type _IntersectAll = T extends [infer First, ...infer Rest] + ? _IntersectAll> + : R + +/* + * + * External/Copied Utility Types + * + */ + +/** + * Removes field names from a tuple + * Source: https://stackoverflow.com/a/63571175/62937 + */ +type RemoveNames = [any, ...T] extends [ + any, + ...infer U +] + ? U + : never + +/** + * Transposes nested arrays + * Source: https://stackoverflow.com/a/66303933/62937 + */ +type Transpose = T[Extract< + keyof T, + T extends readonly any[] ? number : unknown +>] extends infer V + ? { + [K in keyof V]: { + [L in keyof T]: K extends keyof T[L] ? T[L][K] : undefined + } + } + : never diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts new file mode 100644 index 00000000..51e19dfa --- /dev/null +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -0,0 +1,44 @@ +// This entire implementation courtesy of Anders Hjelsberg: +// https://github.com/microsoft/TypeScript/pull/50831#issuecomment-1253830522 + +type UnknownFunction = (...args: any[]) => any + +type LongestTuple = T extends [ + infer U extends unknown[] +] + ? U + : T extends [infer U, ...infer R extends unknown[][]] + ? MostProperties> + : never + +type MostProperties = keyof U extends keyof T ? T : U + +type ElementAt = N extends keyof T + ? T[N] + : unknown + +type ElementsAt = { + [K in keyof T]: ElementAt +} + +type Intersect = T extends [] + ? unknown + : T extends [infer H, ...infer T] + ? H & Intersect + : T[number] + +type MergeTuples< + T extends readonly unknown[][], + L extends unknown[] = LongestTuple +> = { + [K in keyof L]: Intersect> +} + +type ExtractParameters = { + [K in keyof T]: Parameters +} + +export type MergeParameters = + '0' extends keyof T + ? MergeTuples> + : Parameters diff --git a/typescript_test/test.ts b/typescript_test/test.ts index bf7d7e51..9ccafdb5 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -10,7 +10,7 @@ import { OutputSelector, SelectorResultArray, Selector -} from '../src' +} from 'reselect' import microMemoize from 'micro-memoize' import memoizeOne from 'memoize-one' @@ -587,7 +587,7 @@ function testOptionalArgumentsConflicting() { (state: State) => state.baz, (state: State, arg: string) => arg, (state: State, arg: number) => arg, - (baz) => { + baz => { const baz1: boolean = baz // @ts-expect-error const baz2: number = baz @@ -603,7 +603,7 @@ function testOptionalArgumentsConflicting() { const selector2 = createSelector( (state: State, prefix: any) => prefix + state.foo, - (str) => str + str => str ) // @ts-expect-error here we require one argument which can be anything so error if there are no arguments @@ -615,7 +615,7 @@ function testOptionalArgumentsConflicting() { // here the argument is optional so it should be possible to omit the argument or pass anything const selector3 = createSelector( (state: State, prefix?: any) => prefix + state.foo, - (str) => str + str => str ) selector3({} as State) @@ -624,8 +624,9 @@ function testOptionalArgumentsConflicting() { // https://github.com/reduxjs/reselect/issues/563 const selector4 = createSelector( - (state: State, prefix: string, suffix: any) => prefix + state.foo + String(suffix), - (str) => str + (state: State, prefix: string, suffix: any) => + prefix + state.foo + String(suffix), + str => str ) // @ts-expect-error @@ -636,8 +637,9 @@ function testOptionalArgumentsConflicting() { // as above but a unknown 2nd argument const selector5 = createSelector( - (state: State, prefix: string, suffix: unknown) => prefix + state.foo + String(suffix), - (str) => str + (state: State, prefix: string, suffix: unknown) => + prefix + state.foo + String(suffix), + str => str ) // @ts-expect-error @@ -646,21 +648,21 @@ function testOptionalArgumentsConflicting() { selector5({} as State, 'blach') selector5({} as State, 'blach', 4) - // @ts-expect-error It would be great to delete this, it is not correct. - // Due to what must be a TS bug? if the default parameter is used, we lose the type for prefix - // and it is impossible to type the selector without typing prefix - const selector6 = createSelector( - (state: State, prefix = '') => prefix + state.foo, - (str: string) => str - ) - - // because the suppressed error above, selector6 has broken typings and doesn't allow a passed parameter - selector6({} as State) - // @ts-expect-error would be great if we can delete this, it should not error - selector6({} as State, 'blach') - // @ts-expect-error wrong type - selector6({} as State, 1) - + // This next section is now obsolete with the changes in TS 4.9 + // // @ts-expect-error It would be great to delete this, it is not correct. + // // Due to what must be a TS bug? if the default parameter is used, we lose the type for prefix + // // and it is impossible to type the selector without typing prefix + // const selector6 = createSelector( + // (state: State, prefix = '') => prefix + state.foo, + // (str: string) => str + // ) + + // // because the suppressed error above, selector6 has broken typings and doesn't allow a passed parameter + // selector6({} as State) + // // @ts-expect-error would be great if we can delete this, it should not error + // selector6({} as State, 'blach') + // // @ts-expect-error wrong type + // selector6({} as State, 1) // this is an example fixing selector6. We have to add a un-necessary typing in and magically the types are correct const selector7 = createSelector( @@ -679,7 +681,7 @@ function testOptionalArgumentsConflicting() { const selector8 = createSelector( (state: State, prefix: unknown) => prefix, - (str) => str + str => str ) // @ts-expect-error needs a argument diff --git a/typescript_test/tsconfig.json b/typescript_test/tsconfig.json index 1c4f4e58..268d2823 100644 --- a/typescript_test/tsconfig.json +++ b/typescript_test/tsconfig.json @@ -4,7 +4,12 @@ "strict": true, "target": "ES2015", "declaration": true, - "noEmit": true + "noEmit": true, + "skipLibCheck": true, + "paths": { + "reselect": ["../src/index"] // @remap-prod-remove-line + } }, - "include": ["test.ts"] + "include": ["test.ts"], + }