From 508fffa87b3c09686bcca4fd81c2c430b0d04320 Mon Sep 17 00:00:00 2001 From: Peter Cook Bulukin Date: Wed, 15 Mar 2023 10:16:38 +0100 Subject: [PATCH 1/3] Infer correct state when input selectors are mix of explicit and spread The state parameter of the result of createSelector was of type never if input selectors were a mix of explicit and spread selectors --- src/types.ts | 8 +++-- src/versionedTypes/index.ts | 2 +- src/versionedTypes/ts47-mergeParameters.ts | 6 ++-- typescript_test/test.ts | 39 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/types.ts b/src/types.ts index c1ca68641..517700fe7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { MergeParameters } from './versionedTypes' +import type { MergeParameters, ExtractParams } from './versionedTypes' export type { MergeParameters } from './versionedTypes' /* @@ -89,9 +89,13 @@ export type EqualityFn = (a: any, b: any) => boolean export type SelectorResultArray = ExtractReturnType +/** Finds union of the first parameter from an array of functions and turns it into an intersection */ +type GetIntersectedFirstParam = + UnionToIntersection[number][0]> + /** Determines the combined single "State" type (first arg) from all input selectors */ export type GetStateFromSelectors = - MergeParameters[0] + GetIntersectedFirstParam /** Determines the combined "Params" type (all remaining args) from all input selectors */ export type GetParamsFromSelectors< diff --git a/src/versionedTypes/index.ts b/src/versionedTypes/index.ts index b2ff693e9..fa7678e65 100644 --- a/src/versionedTypes/index.ts +++ b/src/versionedTypes/index.ts @@ -1 +1 @@ -export { MergeParameters } from './ts47-mergeParameters' +export { MergeParameters, ExtractParams } from './ts47-mergeParameters' diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 51e19dfa4..76d31797c 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -34,11 +34,9 @@ type MergeTuples< [K in keyof L]: Intersect> } -type ExtractParameters = { +export type ExtractParams = { [K in keyof T]: Parameters } export type MergeParameters = - '0' extends keyof T - ? MergeTuples> - : Parameters + '0' extends keyof T ? MergeTuples> : Parameters diff --git a/typescript_test/test.ts b/typescript_test/test.ts index 0b44df121..9b5d76e2d 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -1665,3 +1665,42 @@ function issue555() { const selectorResult2 = someSelector2(state, undefined) const selectorResult3 = someSelector3(state, null) } + +function issue601() { + interface IState { + foo: number + bar: string + } + + interface IState2 { + bar: string + baz: number + } + + const selectFoo = (state: IState) => state.foo + // Includes params in selector to be sure that this does not ruin inference + const selectBar = (state: IState2, params: { a: number }) => state.bar + + const selectors = [selectFoo, selectBar] + + const selectFooBar = createSelector( + [selectBar, selectFoo, ...selectors], + (foo, bar, ...rest) => { + rest.push('a') + rest.push(1) + // @ts-expect-error + rest.push(true) + // @ts-expect-error + const nFoo: number = foo + // @ts-expect-error + const nBar: string = bar + return 'foobar' + } + ) + + selectFooBar({ bar: 'bar', baz: 2, foo: 3 }) + // @ts-expect-error + const res: number = selectFooBar({ bar: 'bar', baz: 2, foo: 3 }) + // @ts-expect-error + selectFooBar({ bar: 'bar', foo: 3 }) +} From 62406a68192fb9cda8343cfbcb8bc485640c780b Mon Sep 17 00:00:00 2001 From: PeterBul Date: Thu, 16 Mar 2023 15:57:31 +0100 Subject: [PATCH 2/3] Modify test to represent the existing type inference Previous test implies that different input state types are not supported in inference, only the state of the first selector is picked. Advanced tests for different input states in selectors therefore have to be removed since typescript v4.6 and before does not support it. Additionally, parameter is added to test, to test that the parameter is included in the output selector paraemeters. --- typescript_test/test.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/typescript_test/test.ts b/typescript_test/test.ts index 9b5d76e2d..a121ea3ab 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -1672,14 +1672,9 @@ function issue601() { bar: string } - interface IState2 { - bar: string - baz: number - } - const selectFoo = (state: IState) => state.foo // Includes params in selector to be sure that this does not ruin inference - const selectBar = (state: IState2, params: { a: number }) => state.bar + const selectBar = (state: IState, params: { a: number }) => state.bar const selectors = [selectFoo, selectBar] @@ -1698,9 +1693,9 @@ function issue601() { } ) - selectFooBar({ bar: 'bar', baz: 2, foo: 3 }) + selectFooBar({ foo: 3, bar: 'bar' }, { a: 3 }) // @ts-expect-error - const res: number = selectFooBar({ bar: 'bar', baz: 2, foo: 3 }) + const res: number = selectFooBar({ foo: 3, bar: 'bar' }) // @ts-expect-error - selectFooBar({ bar: 'bar', foo: 3 }) + selectFooBar({ foo: 3, bar: 'bar' }) } From de5831296a7e502b7557a5058d67c90b0df38709 Mon Sep 17 00:00:00 2001 From: PeterBul Date: Thu, 16 Mar 2023 16:01:43 +0100 Subject: [PATCH 3/3] Make mergeParameters work with a combination of explicit selectors and rest syntax This makes sure that extra parameters from input selectors are not lost when using a combination of explicit parameters and rest syntax. --- src/types.ts | 32 ++++++++++--- src/versionedTypes/index.ts | 2 +- src/versionedTypes/ts47-mergeParameters.ts | 52 ++++++++++++++++------ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/types.ts b/src/types.ts index 517700fe7..9a117f8b3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { MergeParameters, ExtractParams } from './versionedTypes' +import type { MergeParameters } from './versionedTypes' export type { MergeParameters } from './versionedTypes' /* @@ -71,7 +71,8 @@ export type OutputParametricSelector< Result, Combiner extends UnknownFunction, Keys = {} -> = ParametricSelector & OutputSelectorFields +> = ParametricSelector & + OutputSelectorFields /** An array of input selectors */ export type SelectorArray = ReadonlyArray @@ -89,13 +90,9 @@ export type EqualityFn = (a: any, b: any) => boolean export type SelectorResultArray = ExtractReturnType -/** Finds union of the first parameter from an array of functions and turns it into an intersection */ -type GetIntersectedFirstParam = - UnionToIntersection[number][0]> - /** Determines the combined single "State" type (first arg) from all input selectors */ export type GetStateFromSelectors = - GetIntersectedFirstParam + MergeParameters[0] /** Determines the combined "Params" type (all remaining args) from all input selectors */ export type GetParamsFromSelectors< @@ -122,6 +119,27 @@ export type Head = T extends [any, ...any[]] ? T[0] : never /** All other items in an array */ export type Tail = A extends [any, ...infer Rest] ? Rest : never +/** Last item in an array. Recursion also enables this to work with rest syntax - where the type of rest is extracted */ +export type ReverseHead = Tail extends [ + unknown +] + ? S + : Tail extends readonly unknown[][] + ? ReverseHead> + : never + +/** All elements in array except last + * + * Recursion makes this work also when rest syntax has been used + * Runs _ReverseTail twice, because first pass turns last element into "never", and second pass removes it. + **/ +export type ReverseTail = _ReverseTail<_ReverseTail> +type _ReverseTail = Tail extends [unknown] + ? [Head] + : Tail extends unknown[] + ? [Head, ..._ReverseTail>] + : never + /** Extract only numeric keys from an array type */ export type AllArrayKeys = A extends any ? { diff --git a/src/versionedTypes/index.ts b/src/versionedTypes/index.ts index fa7678e65..b2ff693e9 100644 --- a/src/versionedTypes/index.ts +++ b/src/versionedTypes/index.ts @@ -1 +1 @@ -export { MergeParameters, ExtractParams } from './ts47-mergeParameters' +export { MergeParameters } from './ts47-mergeParameters' diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 76d31797c..7ede1163b 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -1,11 +1,11 @@ // This entire implementation courtesy of Anders Hjelsberg: // https://github.com/microsoft/TypeScript/pull/50831#issuecomment-1253830522 +import { ReverseHead, ReverseTail } from '../types' + type UnknownFunction = (...args: any[]) => any -type LongestTuple = T extends [ - infer U extends unknown[] -] +type LongestTuple = T extends [infer U extends unknown[]] ? U : T extends [infer U, ...infer R extends unknown[][]] ? MostProperties> @@ -13,11 +13,9 @@ type LongestTuple = T extends [ type MostProperties = keyof U extends keyof T ? T : U -type ElementAt = N extends keyof T - ? T[N] - : unknown +type ElementAt = N extends keyof T ? T[N] : unknown -type ElementsAt = { +type ElementsAt = { [K in keyof T]: ElementAt } @@ -27,16 +25,42 @@ type Intersect = T extends [] ? H & Intersect : T[number] -type MergeTuples< - T extends readonly unknown[][], - L extends unknown[] = LongestTuple -> = { - [K in keyof L]: Intersect> +type MergeTuples> = { + [K in keyof L]: Intersect< + ElementsAt extends readonly unknown[] ? ElementsAt : never + > } -export type ExtractParams = { +type ExtractParameters = { [K in keyof T]: Parameters } export type MergeParameters = - '0' extends keyof T ? MergeTuples> : Parameters + '0' extends keyof T + ? MergeTuples>> + : Parameters + +type HasRest = number extends S['length'] + ? true + : false + +type HasExplicit = '0' extends keyof S + ? true + : false + +type HasCombined = true extends HasExplicit & + HasRest + ? true + : false + +type MakeRestExplicit = + true extends HasCombined + ? [ + ...ReverseTail, + ReverseHead extends readonly unknown[] + ? ReverseHead[number] + : never + ] + : true extends HasRest + ? [...T] + : T