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

Higher Order Instantiation Expressions don't resolve types correctly #52035

Closed
Thundercraft5 opened this issue Dec 27, 2022 · 7 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@Thundercraft5
Copy link

Thundercraft5 commented Dec 27, 2022

Bug Report

🔎 Search Terms

  • higher order instantiation expression
  • instantiation expressions
  • instantiation expression loses generic
  • nested instantiation expression doesn't resolve
  • nested instantiation expression

🕗 Version & Regression Information

TS 4.7+ (in every version I tried, it seems to be there from start this feature was added).

⏯ Playground Link

Playground link with relevant code

💻 Code

declare function transform<
    const A extends readonly any[], 
    F extends <
        V extends A[number], 
        K extends keyof A,
    >(value: V, index: K, array: A) => any
>(array: [...A], func: F): { 
    [K in keyof A]: ReturnType<typeof func<A[K], K>> 
};

const $2 = transform(["object", "string", "number"], k => ({ type: k }));
    // ^?
    // Should be [{ type: "object" }, { type: "string" }, { type: "number" }], but is [any, any, any]

🙁 Actual behavior

🙂 Expected behavior

  • The instantiation expression typeof func<A[K], K> in transform() should resolve to { type: A[K] }.
  • res should resolve to [{ type: "object" }, { type: "string" }, { type: "number" }].
  • Functions used in higher order instantiation expressions should resolve correctly and not revert to their constraint types.
@Thundercraft5
Copy link
Author

Thundercraft5 commented Dec 27, 2022

Additional repro demonstrating this bug.

P.S. It seems that cases for higher-order expressions weren't included in baselines for instantiation expressions.

@DarrenDanielDay
Copy link

It seems TypeScript currently has no support for higher order types, and it's a design limitation. That means, the following code, or other code with the same functionality (like the example in this issue), will not work as we expected.

type Mapped<Mapper, T> = Mapper<T>; // Type 'Mapper' is not generic. (2315)

It's common to make abstraction like this in JavaScript:

function mapped(mapper, value) {
  return mapper(value);
}
function box(value) {
  return { value };
}
console.log(mapped(box, 'foo'));

That is, we might want to create higher order function called mapped that applies the mapper to the given value, and both mapper and value are not statically typed, and the type Mapped above is what we might want to.

Another situation is class instantiation with constructors and constructor parameters. Generic function works well with normal constructors, but it cannot infer generic instance type with generic constructors:

function create<T extends new (...args: any[]) => any>(ctor: T, ...params: ConstructorParameters<T>): InstanceType<T> {
  return new ctor(...params);
}

class Foo {
  constructor(public message: string) {}
  sayHello() {
    console.log(`hello from ${this.message}`);
  }
}

// Well typed, because `Foo` is not a generic type.
const foo = create(Foo, "bar");
foo.sayHello();
// Not well typed, because `Set` is a generic type, and its constructor parameter uses its generic parameter `T`.
const numberSet = create(Set, [1, 2, 3]);
//    ^^^^^^^^^ Inferred as `Set<unknown>` -- the generic parameter is instantiated with its constraint.

TypeScript 4.7 introduced instantiation expressions, which seems to make that possible: instantiate generic parameters of a generic function by instantiation expressions, and then get the return type of the generic function. But unfortunately, it didn't make it.

I know it's not the first time to introduce higher order types (or higher order generics) in the TypeScript community and it takes a lot of effort to implement this feature, but I still wish that TypeScript would support it, which would absolutely open a new world for the type system like the template literal types feature in TypeScript 4.1✨.

@RyanCavanaugh
Copy link
Member

The thing you mostly need here is call types, not higher-kinded types per se. The very smallest problem is that k => { type: k } is not a generic function, and what needs to happen in this call to resolve as desired is to repeatedly perform separate generic inference rounds on calls to that function expression (once you make it generic, of course).

Tracking somewhat at #52295 (this would be a mapped type rather than a union type, but the logic is pretty similar) and to a greater extent at #6606, which I think we will revisit sometime to specifically think about (generic) functions' call types.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jan 24, 2023
@Thundercraft5
Copy link
Author

I don't think you understood my example; the issue is not strictly mapped or union types. Even though k => { type: K } is not generic, it should be "inhabited" by the generic constraint type (hovering over shows it already does sort of). This inhabiting behavior already happens with satisifies, as in this example:

type Func = <T>(value: T) => any;
const satisfiedFunc = (param => {
  return [
    param
  ] as const;
}) satisfies Func;

type $0 = typeof satisfiedFunc<0>; // outputs correctly as `readonly [0]`

Playground Link

To further illustrate the issue, the expected behavior actually happens when you explicitly type the return type of F (in this case { type: V }:

 // ...
    F extends <
        V extends A[number], 
        K extends keyof A,
    >(value: V, index: K, array: A) => { type: V } | {}
// ...

const items = ["object", "string", "number"] as const;
const $0 = transform(items, k => ({ type: k }));
    // ^?
    // Correctly becomes [{ type: "object" }, { type: "string" }, { type: "number" }], because the type is explicitly typed in the generic constraint
    // It should go to resolve to the type of `F` though

Playground Link

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 26, 2023

What you're seeing there is an illusion. Consider tweaking it to this variant:

interface Lookup {
    object: "obj";
    string: "str";
    number: "num";
}
declare function remap<T extends (typeof items)[number]>(k: T): Lookup[T];
const m = remap("object");
//    ^? 'obj'

const items = ["object", "string", "number"] as const;
const $0 = transform(items, k => ({ type: remap(k) }));

The correct type of $0 would be { type: "obj" } | { type: "num" } | { type: "str" }

Since Lookup could be arbitrarily complex (e.g. a conditional type instead of a lookup type), the only correct way to resolve this would be to iteratively pretend to call the function expression with each type as it goes through [K in keyof A]: ReturnType<typeof func<A[K], K>>

@typescript-bot
Copy link
Collaborator

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

@ntucker
Copy link

ntucker commented Apr 19, 2023

This is a problem:
https://www.typescriptlang.org/play?target=99&jsx=0&ts=5.0.4#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwxilQGdEcYBbAHgCh4H4w8SN4BBeEADwxFWAl4cKMDwQAnvGISA2gF0ANPHqMAYl179B8agDUAfAAoAblAjIQALnh6AlPAC8B6agm1jsIhJuyAdAHsSkhoYDZqdjYA3vCyANLwWPgA1iASOIgc8jYASiAYyDCoACoSAA4g1BjlIBkh6NTs8fIGLgC+ANy0tMykbJmOusXGyVbFDs7wRjHVFTbJ8G12PSxsACQATE4ERKTkVEayAEQ4AEYAVuAYR8pHrDBJAOY38EeoyJSnIDBHwYh2XUY8AA9MD4AA9AD8qgYoPgAGUABY4ZAQYDwL6xGY1GwnC5XI6LZTYuave5PQltYkEHGvd6fb6U+RAA

declare function transform<
    const A extends readonly any[], 
    F extends <V>(value: V) => any
>(array: [...A], func: F): { [K in keyof A]: ReturnType<typeof func<A[K]>> };

const f = <T>(k:T) => ({ type: k })
const $2 = transform(["object", "string", "number"], f);
    // ^?
    // Should be [{ type: "object" }, { type: "string" }, { type: "number" }]

When I change to:

declare function transform<
    const A extends readonly any[], 
    F extends <V>(value: V) => {type: V}
>(array: [...A], func: F): { [K in keyof A]: ReturnType<typeof func<A[K]>> };

const f = <T>(k:T) => ({ type: k })
const $2 = transform(["object", "string", "number"], f);
    // ^?
    // Should be [{ type: "object" }, { type: "string" }, { type: "number" }]

it works - but that assumes we actually know the return value - if we're having the entire F be generic - maybe it'll have a different return type!

declare function transform<
    const A extends readonly any[], 
    F extends <V>(value: V) => {type: V}
>(array: [...A], func: F): { [K in keyof A]: ReturnType<typeof func<A[K]>> };

const f = <T>(k:T) => ({ type: k })
const f2 = <T>(k:T) => ({ blarg: k })
const $2 = transform(["object", "string", "number"], f);
const $3 = transform(["object", "string", "number"], f2);
    // ^?
    // Should be [{ type: "object" }, { type: "string" }, { type: "number" }]

This simply makes the second call fail...but if we make the generic accept that, then it won't give us a return type

Final example with both: https://www.typescriptlang.org/play?target=99&jsx=0&ts=5.0.4#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwxilQGdEcYBbAHgCh4H4w8SN4BBeEADwxFWAl4cKMDwQAnvGISA2gF0ANPHqMAYl179B8agDUAfAAoAblAjIQALnh6AlPAC8B6agm1jsIhJuyAdAHsSkhoYDZqdjYA3vCyANLwWPgA1iASOIgc8jYASiAYyDCoACoSAA4g1BjlIBkh6NTs8fIGLgC+ANy0tMykbJmOusXGyVbFDs7wRjHVFTbJyjyEUDYArPBtdj0s-QBMTkMjYxMu0-AARtAwAObzG1u9rPAAJPuDy6TkVEayAEQ45wAVuAML9lL9WDAktcwfBfqhkJRziAYL9gog7F1GPAAPQ4+AAPQA-KoGHj4ABlAAWOGQEGAFwQshmNRs-yBINhSyINgRSJRG2ULLmcMh0K5vB58D5yJggvgwuscJlKIly15iNlG3k2z6LwAzAcPmQKJQfuzgZhYRDCOLwSrUejdpjSbj8cTXeTqbT6YzYjFLrBbnCAZbQfKA1dgzaoagYRGLlG2Q7ftrurRQJBYAgUOhsHgCERPqbShVgHRsY82JwltohCIxKhJK45MFXRpawIhPpjGYLEr7E4XFFZgO2h4jF4oD5YgE-EFlLmwvAItFYgkkvBUulMkFcvlCiUalUanUl41mq0Nl1dU9ngAWI1Fk1UUsgYDm0Oc8FiuPWlPoi62Lkh6wH4t6dIMsi-oEKyIYclaiySis0qagKbRCrBIoxnamjqqh-JyhhCpYUq8JoaiSH4Q6aZVi86zvM+XyUG+H5-F+iGiraf72hRaKLs6WKMCBJJgZSNKQX6zKJkGbIceGxGRrJXGxvGikyTcyZ8dqQA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants