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

Instantiation expressions #47607

Merged
merged 17 commits into from
Feb 16, 2022
Merged

Instantiation expressions #47607

merged 17 commits into from
Feb 16, 2022

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Jan 26, 2022

With this PR we implement Instantiation Expressions which provide the ability to specify type arguments for generic functions or generic constructors without actually calling them. For example:

function makeBox<T>(value: T) {
    return { value };
};

const makeStringBox = makeBox<string>;  // (value: string) => { value: string }
const stringBox = makeStringBox('abc');  // { value: string }

const ErrorMap = Map<string, Error>;  // new () => Map<string, Error>
const errorMap = new ErrorMap();  // Map<string, Error>

Above, the instantiation expression makeBox<string> changes the type of makeBox from <T>(value: T) => { value: T } to the more specific type (value: string) => { value: string }. When emitted to JavaScript, instantiation expressions simply have the type arguments erased.

Instantiation expressions are particularly useful for creating specific instantiations of generic class constructors such as the ErrorMap above. Previously, this could only be accomplished with a type annotation or a redundant subclass:

const makeStringBox: (value: string) => { value: string } = makeBox;
class ErrorMap extends Map<string, Error> {}

The argument to the typeof type operator can now be an instantiation expression. For example:

type StringBoxMaker = typeof makeBox<string>;  // (value: string) => { value: string }
type ErrorMapConstructor = typeof Map<string, Error>;  // new () => Map<string, Error>

A particularly useful pattern is to create generic type aliases for applications of typeof that reference type parameters in type instantiation expressions:

type BoxFunc<T> = typeof makeBox<T>;  // (value: T) => { value: T }
type Box<T> = ReturnType<typeof makeBox<T>>;  // { value: T }
type StringBox = Box<string>;  // { value: string }

Notice how Box<T> captures the inferred return type of a generic function without loosing the generic type. This was previously not possible.

The type of an instantiation expression f<T> is determined as follows:

  • If f is an object type, f<T> produces an object type in wherein generic signatures for which <T> is an applicable type argument list are instantiated with the given type arguments. Non-applicable signatures, including non-generic signatures, are elided, but other object type members (such as properties) are unaffected. An error occurs if f has signatures, but none for which <T> is an applicable type argument list.
  • If f is a union or intersection type, f<T> produces a union or intersection type where the type argument list <T> has been applied to each constituent.
  • If f is a generic type, f<T> applies the type argument list <T> to the constraint of the type of f.
  • If f is of any other type, f<T> has the same type as f.

Note that an instantiation expression can be applied to any type, but that an error occurs if no call or construct signatures are present in the type. For example, an instantiation expression can be applied to a union that includes undefined:

function f(makeBox: (<T>(value: T) => T) | undefined) {
    const makeStringBox = makeBox<string>;  // ((value: string) => string) | undefined
    if (makeStringBox) {
        const box = makeStringBox('abc');
    }
}

Fixes #37181.
Fixes #40542.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Jan 26, 2022
@Harpush
Copy link

Harpush commented Jan 26, 2022

@ahejlsberg This is super close to a solution of #37181. Will it will allow us to do:

function makeBox<T>(value: T) {
    return { value };
};

// Can we do it or something similar? Currently doesn't compile :(
type MakeBox<T> = ReturnType<typeof makeBox<T>>

// As it now allows us to do (no generics though)
const stringMakeBox = makeBox<string>;
type MakeBox = ReturnType<typeof stringMakeBox>

// And even more relevant to support generics if we can now do:
type MakeBox = ReturnType<typeof makeBox<string>>

Up to now makeBox<string> was illegal without calling it. But now that it is supported allowing typeof on it seems to fit nicely.

@g-plane
Copy link
Contributor

g-plane commented Jan 26, 2022

Building the TypeScript playground for this PR would be better.

@ahejlsberg
Copy link
Member Author

This is super close to a solution of #37181. Will it will allow us to do...

Yes, you can indeed use that pattern to capture a generic return type of a generic function. This is something that previously wasn't possible. I'll update the PR description to include an example.

@Jack-Works
Copy link
Contributor

Let's say we have a function f have a call signature f<T extends number, Q>() and a construct signature new f<T extends string>()

What will this PR behave? They have different argument counts and even different different type constraints

@ahejlsberg
Copy link
Member Author

ahejlsberg commented Jan 27, 2022

Let's say we have a function f have a call signature f<T extends number, Q>() and a construct signature...

This works just fine:

declare const f: {
    <T extends number, Q>(): [T, Q];
    new <T extends string>(): [T];
};

type T0 = typeof f<string>;  // new () = [string]
type T1 = typeof f<number, string>;  // () => [number, string]

But an error occurs here:

declare const g: {
    <T extends number>(): [T];
    new <T extends string>(): [T];
};

type T2 = typeof g<string>;  // Error, type 'string' does not satisfy the constraint 'number'

@ahejlsberg
Copy link
Member Author

@typescript-bot test this
@typescript-bot user test this inline
@typescript-bot run dt
@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

Heya @ahejlsberg, I'm starting to run the inline community code test suite on this PR at 8e466ba. Hold tight - I'll update this comment with the log link once the build has been queued.

@tonyxiao
Copy link

Did this feature get released in 4.7? Somehow when I try the example with ts version 4.7.4 (latest as of today) it complains in the editor.

CleanShot 2022-07-21 at 09 40 21@2x

@orta
Copy link
Contributor

orta commented Jul 21, 2022

Yep, works in the playground - given that those inline error messages dont come from the official typescript extension (to my knowledge)- it's likely a local setup issue causing this

image

@DrSensor
Copy link

It doesn't works on build-ins method 🤔
https://www.typescriptlang.org/play?jsx=0#code/FAYw9gdgzgLgBCAhiAFgUzgXjhNB3OAdTUQGsBZRABwB5g44BBAGnrgCU0YBXAJwgAqATypoaMEWjAAzOAHkARgCs0IGAG0ARAHMucvBAAKvMKN4SAImigheASyowwvKJoC6NRgD4vcAPR+cGi8JrzAXgAUAJQA3MCgkLBwiFhwAN5wCgBccACMcAC+wBKiTKklUrKI8eDQ8AAm1iCUVKmKKmoAdLow+kYmZpZN9o7OUJ6RiFFAA

const cache = new WeakMap<
  A,
  ReturnType<typeof Object["getOwnPropertyDescriptors"]<A>> // error
>();

const a = { b: 1 }
type A = typeof a

const descMap = Object.getOwnPropertyDescriptors<A>(a)
    'A' only refers to a type, but is being used as a value here.
    Variable 'ReturnType' implicitly has an 'any' type.
    Conversion of type '{ b: number; }' to type '<T>(o: T) => { [P in keyof T]: TypedPropertyDescriptor<T[P]>; } & { [x: string]: PropertyDescriptor; }' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
      Type '{ b: number; }' provides no match for the signature '<T>(o: T): { [P in keyof T]: TypedPropertyDescriptor<T[P]>; } & { [x: string]: PropertyDescriptor; }'.
    ',' expected.
    '>' expected.
    Expression expected.
    Expression expected.
    Expression expected.

@herjiict
Copy link

herjiict commented Nov 30, 2022

How to use this feature with overloaded functions ?

declare function foo<T extends keyof HTMLElementTagNameMap>(arg: T): HTMLElementTagNameMap[T];
declare function foo<T extends HTMLElement>(arg: T): T;
type FirstFoo = typeof foo<"div">;    // error
const f: ReturnType<FirstFoo> = foo("div");

I couldn't get the code to work. The error message is Type 'string' does not satisfy the constraint 'HTMLElement'.
Why does it have to satisfy both foos ? How to pick the first foo ?

https://www.typescriptlang.org/play?ts=4.9.3#code/CYUwxgNghgTiAEAzArgOzAFwJYHtVJxwB4AVeEADwxFWAGd4BrEATx0XgAkSBZAGQCiEEAFsaGElADmAOShieUAA4A+ABSwpALngkAlDu78ho8ZNnyQipQG0SAXQDcAKFCRYCFOmx4CxMpTUtAxGgsJiqBjqmjr6sS4YLEoIAGJYMHQYKYTwALzwicnsfkQARMBYAG6lKi5geJlIOgBKIBjIMKgkSSBEaRlZhCp5fmrlVaV6LkA

The code works if I change the second foo to declare function foo<T extends HTMLElement, U>(arg: T, _unused: U): T;.
But I can't get it to work if foo have only one parameter. (Is there still something ambiguous to the compiler in this case that prevent the compiler from picking the first foo ?)

@Thundercraft5
Copy link

Thundercraft5 commented Jan 17, 2023

Higher-order instantiation expressions don't resolve properly, as they weren't accounted for in the test baselines (see also issue #52035).

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]

https://www.typescriptlang.org/play?target=99&jsx=0&ts=5.0.0-dev.20221220#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwxilQGdEcYBbAHgCh4H4w8SN4BBeEADwxFWAl4cKMDwQAnvGISA2gF0ANPHqMAYl179B8Oo33wAapr4Ch7WamSUARiBhKVBxgGkT2oQGsQEnIg6KqgwAfAAUAG5QEMggAFxGylgCPPEuyrBEEvHsAJTwALzB0qgStGEZUFnwsgB0deyOKOjxajnxAN7Vbknw3r7+DfEASiAYyDCoACoSAA4g1BizIH5IaGDUFi6OLsFFAL4A3LS0zKRsACQATAUERKTkVKGyAEQ4NgBW4BjPys+sMEkAOY-eDPKy2ezPRyeApFUKdRZzeIwvY5HJHfQAekx8AAegB+ILwbHwADKAAscMgIMB4HZqgilvFXh8vs94HtlIykaD-kD2Zz4Ny4qDwXYYAL5EA

@jedwards1211
Copy link

jedwards1211 commented May 16, 2023

Correct, instantiation expressions currently have no equivalent in type land.

Is there any issue tracking a design for a way we could do that?

I've been trying to do something that I'd be able to accomplish if I could have a type like

// type ApplyFn<Fn, Arg> = (some magic...)
type Applied = ApplyFn<<T>(value: T) => { value: T }, 1 | 2 | 3> // { value: 1 | 2 | 3 }

Someone correct me if I'm wrong but it seems there's no way to do this for an arbitrary function.

@iFwu
Copy link

iFwu commented Aug 4, 2023

It also solves #20719.

@darrylnoakes
Copy link

Correct, instantiation expressions currently have no equivalent in type land.

Is there any issue tracking a design for a way we could do that?

I've been trying to do something that I'd be able to accomplish if I could have a type like

// type ApplyFn<Fn, Arg> = (some magic...)
type Applied = ApplyFn<<T>(value: T) => { value: T }, 1 | 2 | 3> // { value: 1 | 2 | 3 }

Someone correct me if I'm wrong but it seems there's no way to do this for an arbitrary function.

I don't believe there is. Instantiation expressions only work for values; you can't use a similar method in types.
I tried to work around the lack of higher-order generics by using a generic function type, but I think the core limitations are the same, so it's not supported. The function-type type parameter is said to be not generic.

type Fn = <Arg>() => unknown
type ApplyFn<T, F extends Fn> = T extends unknown ? ReturnType<F<T>> : never

const f = <T>(): [T] => ({} as [T])

type t1 = ApplyFn<1 | 2, typeof f>

The following (direct instead of the type parameter) works:

type ApplyFn<T> = T extends unknown ? ReturnType<typeof f<T>> : never

type t1 = ApplyFn<1 | 2>
//   ^? [1] | [2]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Apply function generics as part of type definition Allow binding generic functions to a given type