2

Initially, I wanted to type redux' mapDispatchToProps-like function and got stuck with handling an array of functions (action creators) as an argument. There is a hacky but working example. I wonder if it can be improved.

The problem is that return type does not preserve types per item, it gets generalized to a union.

Update: the problem is in preserving parameter types per function in the resulting array.

In short, code below should be with no errors:

type F = (...x: any[]) => any
type Wrap<T extends F> = (...x: Parameters<T>) => void

const wrap = (fn: any) => (...a: any) => {fn(...a)}

// currently working solution
// the problem is K being too wide (number | string | symbol) for array index thus silenced
// // @ts-expect-error
// function main<Fs extends readonly F[]>(fs: Fs): {[K in keyof Fs]: Wrap<Fs[K]>}

// TODO: desired solution but not finished: every item in `fs` should be wrapped with `Wrap`
function main<Fs extends readonly F[]>(fs: Fs): [...Fs]

function main(fs: any) {return fs.map(wrap)}

const n = (x: number) => x
const s = (x: string) => x
const fs = main([n, s] as const)

// TEST PARAMETERS TYPES
fs[0](1)
fs[1]('1')
// @ts-expect-error
fs[0]('1')
// @ts-expect-error
fs[1](1)

// TEST RETURN TYPES
const _1: void = fs[0](1)
const _2: void = fs[1]('1')
// @ts-expect-error
const _3: number = fs[0](1)
// @ts-expect-error
const _4: string = fs[1]('1')

TypeScript playground

P.S: there is an open (as for 25-aug-2020) github issue related to problem of my solution #1, so it is not about variadic tuple types but about keyof ArrayType being too wide for an array index type

0

3 Answers 3

2

TypeScript 4.0 can indeed represent variadic tuple types so you can annotate your main function to indicate what it does; maybe something like this:

const main = <F extends any[]>(f: F): [...F] => [...f];

(If you want to make it F extends Function[] or some specific subtype of function that should work too, but for this example I'm just using any instead of a function type.) Now your example above behaves as you want:

const n = (x: number) => x
const s = (x: string) => x
const [n2, s2] = main([n, s])
n2(1) // okay
s2('1') // okay
n2('1') // error
s2(1) // error

Playground link.

But you could get something like this behavior for TypeScript 3.9 and below by asserting that the return type is the same as the input type, which is close enough to what you're doing and behaves the same (the difference between [...F] and F is small enough not to worry about in most cases):

const main = <F extends any[]>(f: F) => [...f] as F;

Playground link


Note that sometimes the compiler doesn't infer tuple types where you want it to without hints; by writing const [n2, s2] = main([n, s]); you're hinting that you want tuples, so you get tuples.

Otherwise, const n2s2 = main([n, s]) would just be a unordered array type whose elements are of a union type. You could give a hint in the type signature of main by making the F type parameter include a tuple type in its constraint:

const main = <F extends any[] | []>(f: F) => [...f] as F;

(see microsoft/TypeScript#27179 for this idea) which makes this work:

const n2s2 = main([n, s]);
// const n2s2: [(x: number) => number, (x: string) => string]

Playground link


So, some combination of those techniques should hopefully overcome the problem you're having.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for a detailed answer, but I updated my question with little more details: there is also a mapper function
Also, I was unaware of the difference of tuple declaration - seems like a middle way between const n2s2 = main([n, s]) and const n2s2 = main([n, s] as const)
1

You actually don't need TS4 variadic tuples at all. just mapped (tuple) types. The following works in TypeScript 3.5 (though // @ts-expect-error is a 3.9 feature):

function main<Fs extends readonly F[]>(fs: Fs):
    { [K in keyof Fs]: Fs[K] extends F ? Wrap<Fs[K]> : never }

Playground Link


Update: On array (rather than tuple) types, the above will return an array type where the individual indices are no longer known. Once the tuple indices are lost there is no way to recover this information. You can use as const to prevent the initial loss of the tuple information:

const test3 = [
    console.log,
    (x: number) => x,
] as const;
const fs3 = main(test3);
// const fs3: readonly [Wrap<(...data: any[]) => void>, Wrap<(x: number) => number>]

If you want to be sure you aren't losing the indices, it is possible to reject array types and only allow tuples using a bit of a hack:

function main<Fs extends readonly F[]>(
    fs: number extends Fs['length'] ? never : Fs):
    { [K in keyof Fs]: Fs[K] extends F ? Wrap<Fs[K]> : never }

(edit: K in Exclude<keyof Fs, keyof []> -> K in keyof Fs like above, not sure why I had that)
Like above, all of this works in TS3.

Playground Link

9 Comments

Thanks for your input, but it is not the case since even in your added example type of fs2 is const fs2: Wrap<(...data: any[]) => void>[], i.e. types are broken
@ts-expect-error is here to annotate expected error, and if there is no error in the line below then this comment itself becomes an error. So, you still have to check it on [email protected]+
Are you saying you want to only allow input that can have the tuple type inferred and create an error if only a list type can be inferred?
Yes, I want to pass in a list of functions A and get back a list of wrapped functions B each with exact same parameter types as in A. Not widened union type which is TS's default type inference behavior.
Seems like the Exclude<keyof Fs, keyof []> was removing the @@iterator, removed: tsplay.dev/1WGRVm
|
1

You need to let typescript infer the tuple type. Reading up on Variadric Tuples, it looks you need to constrain the generic type parameter to type any[] and then return a spread of that generic parameter:

type Main = <T extends any[]>(fns: T) => [...T]

const main: Main = (fns) => [...fns]

Now the rest works just like you expect.

Playground

1 Comment

Thanks for a suggestion, I updated my question with little more details: there is also a mapper function

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.