Skip to content

Commit

Permalink
Rewrite MergeParameters to work with TS 4.9
Browse files Browse the repository at this point in the history
Per microsoft/TypeScript#50831 , the existing
implementation of `MergeParameters` broke with TS 49 due to a change
in how "optional" and "undefined" arguments get interpreted

Anders Hjelsberg himself supplied a new implementation that's actually
much simpler, but only works with TS 4.7 and greater.

Normally this would require shipping two entirely different sets of
types, as we already do for TS <4.1. However, there's a trick we used
with RTK where we:

- Create a folder and put two different files with different impls of
  the same type inside
- Add an index file and re-export one of those
- Add a file named `package.dist.json` containing `typesVersions` that
  points to both of those `.d.ts` files
- Copy that `package.dist.json` over to `dist/some/package.json` during
  the actual build/publish step

That way during dev TS just imports the type as normal, but the built
version sees that `some/package.json`, sees `typesVersions`, finds the
right `.d.ts` file, and imports the right implementation for itself.
  • Loading branch information
markerikson committed Nov 3, 2022
1 parent 5b83d04 commit 7653e5c
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 189 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Expand Up @@ -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",
Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/build-and-test-types.yml
Expand Up @@ -62,15 +62,25 @@ 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 file that only builds with TS 4.7+'
if: ${{ matrix.ts < 4.7 }}
run: rm src/versionedTypes/ts47-mergeParameters.ts

# 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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
175 changes: 17 additions & 158 deletions src/types.ts
@@ -1,3 +1,6 @@
import type { MergeParameters } from './versionedTypes'
export type { MergeParameters } from './versionedTypes'

/*
*
* Reselect Data Types
Expand Down Expand Up @@ -94,56 +97,6 @@ export type GetParamsFromSelectors<
RemainingItems extends readonly unknown[] = Tail<MergeParameters<S>>
> = 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<T>,
// 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<ParamsArrays>,
// 3) Turn the union of arrays back into a nested tuple. Order does not matter here.
TuplifiedArrays extends any[] = TuplifyUnion<TransposedArrays>,
// 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<TuplifiedArrays>
> =
// 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
Expand All @@ -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> = T extends EmptyObject ? never : T

/** Extract the parameters from all functions as a tuple */
export type ExtractParams<T extends readonly UnknownFunction[]> = {
[index in keyof T]: T[index] extends T[number] ? Parameters<T[index]> : never
}

/** Extract the return type from all functions as a tuple */
export type ExtractReturnType<T extends readonly UnknownFunction[]> = {
[index in keyof T]: T[index] extends T[number] ? ReturnType<T[index]> : never
}

/** Recursively expand all fields in an object for easier reading */
export type ExpandItems<T extends readonly unknown[]> = {
[index in keyof T]: T[index] extends T[number] ? Expand<T[index]> : never
}

/** First item in an array */
export type Head<T> = T extends [any, ...any[]] ? T[0] : never
/** All other items in an array */
Expand All @@ -191,58 +127,6 @@ export type List<A = any> = ReadonlyArray<A>

export type Has<U, U1> = [U1] extends [U] ? 1 : 0

/** Select the longer of two arrays */
export type Longest<L extends List, L1 extends List> = L extends unknown
? L1 extends unknown
? { 0: L1; 1: L }[Has<keyof L, keyof L1>]
: never
: never

/** Recurse over a nested array to locate the longest one.
* Acts like a type-level `reduce()`
*/
export type LongestArray<S extends readonly any[][]> =
// If this isn't a tuple, all indices are the same, we can't tell a difference
IsTuple<S> 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<S[0], S[1]>
: // 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<S[0], S[1]>,
Rest extends any[][] ? LongestArray<Rest> : []
>
: // 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<T extends any[]> = IsTuple<T> extends '0'
? T[0]
: _IntersectAll<T>

type IfJustNullish<T, True, False> = [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, R = unknown> = T extends [infer First, ...infer Rest]
? _IntersectAll<Rest, IfJustNullish<First, R, R & First>>
: R

/*
*
* External/Copied Utility Types
Expand All @@ -253,32 +137,22 @@ type _IntersectAll<T, R = unknown> = 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<Union> = (
export type UnionToIntersection<Union> =
// `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<T extends readonly any[]> = [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
Expand Down Expand Up @@ -313,7 +187,7 @@ type LastOf<T> = UnionToIntersection<
: never

// TS4.1+
type TuplifyUnion<
export type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false
Expand All @@ -331,21 +205,6 @@ export type ObjValueTuple<
? ObjValueTuple<T, KT, [...R, T[K & keyof T]]>
: R

/**
* Transposes nested arrays
* Source: https://stackoverflow.com/a/66303933/62937
*/
type Transpose<T> = 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[]> = T extends [unknown, ...infer U]
? U
Expand Down
1 change: 1 addition & 0 deletions src/versionedTypes/index.ts
@@ -0,0 +1 @@
export { MergeParameters } from './ts47-mergeParameters'
14 changes: 14 additions & 0 deletions src/versionedTypes/package.dist.json
@@ -0,0 +1,14 @@
{
"typesVersions": {
">=4.7": {
"index": [
"./ts47-mergeParameters.d.ts"
]
},
"<4.7": {
"index": [
"./ts46-mergeParameters.d.ts"
]
}
}
}

0 comments on commit 7653e5c

Please sign in to comment.