Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

homomorphic mapped tuple doesn't preserve parallel tuple #46741

Closed
tadhgmister opened this issue Nov 9, 2021 · 4 comments
Closed

homomorphic mapped tuple doesn't preserve parallel tuple #46741

tadhgmister opened this issue Nov 9, 2021 · 4 comments
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@tadhgmister
Copy link

tadhgmister commented Nov 9, 2021

Bug Report

🔎 Search Terms

homomorphic mapped tuples, variadic tuple, parallel lists, zip tuples together, pipeline, compose function, keyof array.

🕗 Version & Regression Information

  • This is the behaviour in every version of the playground from 4.0.5 up to nightly v4.6.0-dev2021-11-05
    • This is not applicable before 4.0.5 since tuple spread [A,...V] doesn't exist that far back
    • and I reviewed the FAQ for entries about mapped types, I don't have primitives here.

⏯ Playground Link and Explanation

Playground link with relevant code

💻 Code

interface A{a:any}
interface B{b:any}
interface C{c:any}

type Combine<X,Y> = {[K in keyof X]: [X[K], Y[K&keyof Y]]}
type XX = [A,B,C]
type YY = [1,2,3]
declare const example_value: [["x", "y"], [A, 1], [B, 2], [C, 3]]

let works_as_expected: Combine<['x',...XX], ['y',...YY]> = example_value
// let works_as_expected: [["x", "y"], [A, 1], [B, 2], [C, 3]]

type Alias<X extends any[],Y extends any[]> = Combine<['x',...X],['y',...Y]>
let doesnt_work: Alias<XX,YY> = example_value
// let doesnt_work: [["x", "y"], [A, "y"], [B, 1], [C, 2]]

🙁 Actual behavior

works_as_expected is correctly the type [['x','y'], [A, 1], [B, 2], [C, 3]] but doesnt_work which is basically defined the exact same way except the spreading is done inside another type alias gives a totally different and incorrect type [["x", "y"], [A, "y"], [B, 1], [C, 2]]

🙂 Expected behavior

example should compile without errors, the type of doesnt_work should be the same as works_as_expected.

@tadhgmister tadhgmister changed the title mapped tuples together homomorphic mapped tuple doesn't preserve parallel tuple Nov 9, 2021
@tadhgmister
Copy link
Author

tadhgmister commented Nov 9, 2021

I came across this while trying to do variadic tuples with a pipeline type function, playground

type Fn<P,R> = (arg:P)=>R
interface A{a:any} // interfaces for testing
interface B{b:any}
interface C{c:any}
interface D{d:any}

// Fns<[A,B,C], [1,2,3]> == [Fn<A,1>, Fn<B,2>, Fn<C,3>]
// these take a list of arguments and a list of return types and basically zips them together
type Fns_RetDriven<Args,Rets> = {[K in keyof Rets]: Fn<Args[K&keyof Args], Rets[K]>}
type Fns_ArgDriven<Args,Rets> = {[K in keyof Args]: Fn<Args[K], Rets[K&keyof Rets]>}

type Fns<Args,Rets> = Fns_RetDriven<Args,Rets>
                      & Fns_ArgDriven<Args,Rets>
// the real use case is a compose/pipeline function which seems to be infering generics properly but the type is just breaking
declare function pipeline<Init,V extends any[],Ret>(...fns: Fns<[Init,...V],[...V,Ret]>): Fn<Init,Ret>

declare const clearly_the_expected_type: Fns<[A,B,C], [B,C,D]>
pipeline(...clearly_the_expected_type)
//Err Argument of type '[Fn<A, B>, Fn<B, C>, Fn<C, D>]' is not 
// assignable to parameter of type 'Fns<[A, B, C], [B, C, D]>'.

That pipeline function is correctly inferring generics in most cases I throw at it, if it wasn't for the correctly inferred generics producing a different and invalid type this would work great. (you can tell it is clearly invalid because the error says it isn't assignable to the type it was literally declared with) I think if this issue is fixed we may finally have the variadic compose and pipeline signature that can infer some arguments of the in-between functions.

@ahejlsberg ahejlsberg self-assigned this Nov 9, 2021
@andrewbranch andrewbranch added the Needs Investigation This issue needs a team member to investigate its status. label Nov 11, 2021
@ahejlsberg
Copy link
Member

This one is interesting.

When instantiating a mapped type that is applied to a tuple type with variadic elements, we transform M<[A, ...T, B]> into [...M<[A]>, ...M<T>, ...M<[B]>], and then rely on tuple type normalization to resolve the non-generic parts of the resulting tuple. So, say that M is a mapped type that "boxes" every element:

type M<T> = { [K in keyof T]: { value: T[K] } };

The instantiation M<[number, ...T, string]> then turns into [{ value: number }, M<T>, { value: string }]. This is generally a desirable transformation because it ensures that a mapped type applied to a variadic tuple type is itself recognized as a tuple type, plus it reduces the genericity of the type and allows better checking of and inference from the non-generic parts. However, the transformation means that the iteration type variable (K in example above) can really only be trusted to represent a property name in the type being iterated over (T in example above), and cannot be trusted to always correlate to an index in some other tuple type or a final combined tuple type.

Here's a simpler example:

type Foo<T> = { [K in keyof T]: K };

type Bar<A extends any[]> = Foo<[0, ...A]>;  // [0, ...Foo<A>] because of transformation

type T0 = Foo<[0, 0, 0, 0]>;  // ["0", "1", "2", "3"]
type T1 = Foo<[0, ...[0, 0, 0]]>;  // ["0", "1", "2", "3"]
type T2 = Bar<[0, 0, 0]>;  //["0", "0", "1", "2"]

Above, in the T1 example, the type [0, ...[0, 0, 0]] is normalized to [0, 0, 0, 0] before Foo<T> is applied to the tuple. But in the T2 example, the tuple is split into two parts and the indices now shift.

I can see how that is a bit surprising, but on the other hand, getting rid of the transformation has significant drawbacks. So, for now I'm going to say this is working as intended.

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Jan 12, 2022
@tadhgmister
Copy link
Author

Intersting, thanks for the further information. Knowing this is how typescript is resolving the tuple spreading I can get the correct logic with this code which still doesn't quite give the auto-magical argument inference I was hoping but is functionally correct which is always good.

I suspect it still isn't working as I'd like because of the need to call Tail which uses infer on the generic it is trying to resolve meaning to figure out what V is you have to already know what V is, I'm honestly really impressed with how well typescript manages the resolution already but if you can't just zip tuples together then I don't think this use case will ever work exactly as intended.

Anyway thanks again for looking into this. Really appreciate typescript and your work to make it great! :)

type Tail<T extends unknown[]> = T extends [any, ...infer Rest] ? Rest : never;
type Fn<P,R> = (param:P)=>R;
/** Borrowed from SimplyTyped */
type Prev<T extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62][T];


type Fns_ArgDriven<Args extends unknown[], Rets extends unknown[]> = {[K in keyof Args]: Fn<Args[K], Rets[K&keyof Rets]>}
type Fns_RetDriven<Args extends unknown[], Rets extends unknown[]> = {[K in keyof Rets]: Fn<Args[K&keyof Args], Rets[K]>}

type Fns<Init, V extends unknown[], Ret> = V extends [] ? [Fn<Init, Ret>]
    : [Fn<Init, V[0]>, ...Fns_ArgDriven<V, [...Tail<V>, Ret]>] & [...Fns_RetDriven<[Init,...V], V>, Fn<V[Prev<V["length"]>], Ret>]

declare function pipeline<Init, V extends unknown[], Ret>(...fns: Fns<Init, V, Ret>): Fn<Init, Ret>

// first test only type annotate the first one, 2nd gets argument infered, 3rd onward only gets unknown infered for parameter type
// as such final return is not captured correctly
const test1 = pipeline((a:number)=>a.toExponential(), 
                        (b)=>b.toUpperCase(), 
                    //  ^?
                        (c)=>({foo:c}))   
                    //  ^?

// second test, annotate first and third, now 2nd is set to unknown
// gives type error that methods can't be used on unknown but return is correct
const test2 = pipeline((a:number)=>a.toExponential(), 
                        (b)=>b.toUpperCase(), 
                    //  ^?
                        (c: string)=>({foo:c}))   
                    //  ^?

declare const should_very_much_work: [Fn<1,2>, Fn<2,3>, Fn<3,4>]
const yay_it_works = pipeline(...should_very_much_work)
//                    ^?

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants