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

WIP - Allow infer types in expression type argument positions #22368

Closed
wants to merge 3 commits into from

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Mar 7, 2018

This allows you to write infer T anywhere in the types inside a call or new expression argument list and have that position inferred. Additionally, the same T may be referenced elsewhere in the arguments and be reused as may be expected (with only the infer'd declaration as sole inference site), since type parameters are able to reference one another, so, too, would be the expectation of arguments:

declare function merge<A, B, C>(x: A, y: B, z: { z: C }): A & B & C;

const result = merge<infer A, {x: string}, A>({y: 12}, {x: "yes"}, {z: {y: 12}});

Enables #10571

If this is too complex a feature (though it's really neat, since you can infer one argument (or just part of one!) while also locking another argument into a corresponding shape), it's possible to either scale back to, or in addition to this, allow omitted type parameters to implicitly be an infer'd type that works similarly to still enable #10571. In any case, I'd like to discuss this direction.

Incidentally, I believe this fixes #21836.

@appsforartists
Copy link

appsforartists commented Mar 7, 2018

So for example, this (based on actual code):

export function withManyMixins<T, S extends Constructor<Observable<T>>>(
    superclass: S): S & Constructor<MixedTogether<T>> {
  
  const result1 = withFirstMixin<T, S>(superclass);
  const result2 = withSecondMixin<T, typeof result1>(result1);
  const result3 = withThirdMixin<T, typeof result2>(result2);

  return result3;
}

could be shortened to:

export function withManyMixins<T, S extends Constructor<Observable<T>>>(
    superclass: S): S & Constructor<MixedTogether<T>> {
  return withThirdMixin<T, infer S2>(
    withSecondMixin<T, infer S1>(
      withFirstMixin<T, S>(superclass)
    )
  );
}

with this PR, correct?

Can the name of the inferred parameter be omitted if that variable isn't used anywhere? Otherwise, I need to make up unique names like S1 and S2 that aren't actually useful.

@cameron-martin
Copy link

cameron-martin commented Mar 7, 2018

Yeah it'd be nice to have the binding to a type variable optional, because most of the time you won't need this and as @appsforartists said it just means making up names that aren't actually used.

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 7, 2018

Does this have the same joining behaviour for multiple occurrences of infer A as conditional types? So the following two are different:

declare function swap<A,B>(pair: [A,B]): [B,A];
const result1 = swap<infer A, A>(['left', true]); // not ok
const result2 = swap<infer A, infer A>(['left', true]); // A = boolean | string

Having syntactic sugar for infer A, where A does not appear anywhere else, would be nice -- I think previous suggestions of * are the best.

Is it the case that the following two behave the same for all generic functions?

declare function swap<A,B>(pair: [A,B]): [B,A];
const result1 = swap<infer A, infer B>(....); // explicit
const result2 = swap(...); // implicit

@weswigham
Copy link
Member Author

weswigham commented Mar 9, 2018

@jack-williams

Is it the case that the following two behave the same for all generic functions?

declare function swap<A,B>(pair: [A,B]): [B,A];
const result1 = swap<infer A, infer B>(...); // explicit
const result2 = swap(...); // implicit

Yes and no. In that case, mostly yes, but suppose you have something overloaded on generic arity:

declare function merge<A,B,C>(root: A, last: B & C): A & B;
declare function merge<A,B>(root: A, last: B & {x: any}): A & B;
const result1 = merge<infer A, infer B>(...); // Selects 2-arity overload
const result2 = merge<infer A, infer B, infer C>(...); // Selects 3-arity overload
const result3 = merge(...); // implicitly attempts inference for all type-argument arities

you can select a specific generic arity which is something not possible with an un-annotated call.

@jakearchibald
Copy link

I'm having trouble getting my head around this, but here's my use-case:

export class Store {
    _map = new Map();

    do<Type, R, A1>(f: <Type>(a1: A1) => R, a1: A1): R
    do<R>(f: (...args: any[]) => R, ...args: any[]): R {
        return f.call(this, ...args);
    }
}

let defaultStore: Store;

function getDefaultStore() {
    if (!defaultStore) defaultStore = new Store();
    return defaultStore;
}

export function get<Type>(this: any, key: any): Promise<Type> {
    const store = this instanceof Store ? this : getDefaultStore();
    return Promise.resolve(this._map.get(key));
}

I'm hoping to be able to call:

const store = new Store();
const val = store.do<string>(get, 'foo');

Where Type can be specified, but R and A1 can be inferred. The user of store doesn't need to be aware of R and A1 at all.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 31, 2018

As per the discussion in #23045, we should close this one, and do named-type arguments + partially specified type argument list with inference

@MeirionHughes
Copy link

MeirionHughes commented Apr 18, 2018

I have a use case I think this PR will fix: consider the following (simplified):

interface IBuilder<I> {
  use(h: <II extends I = I>(ctx: II) => void): IBuilder<I>;
}

let app: IBuilder<{ name: string }>;

If you don't specify any type information for ctx, then it is inferred automatically. If you do type it explicitly with something that runs contrary to the constaints

app.use((ctx: { invalid: number }) => { });

... then you get a property missing in type error.

The problem arises when you want to introduce a generic

app.use(<T>(ctx:T) => { });

at this point ctx is just the generic T, when at the very least it must be the builder's I type. In fact it loses the constraint checking altogether as the following is now accepted:

app.use(<T>(ctx: T & { invalid: number }) => { });

I'm hoping this PR would facilitate app.use(<infer T>(ctx:T) => { });?

@masaeedu
Copy link
Contributor

@mhegazy Is there already an issue to track doing "named type parameters + partially specified argument list"?

@mhegazy
Copy link
Contributor

mhegazy commented Apr 20, 2018

Is there already an issue to track doing "named type parameters + partially specified argument list"?

No. it has been on my list to put a proposal out with the details discussed in #20398. The idea is to do both, allow partially specified type argument list (positional), and allow for named type arguments, and in both cases do inference on the unspecified parameters.

I am using issue #10571 to track it meanwhile.

@weswigham weswigham closed this Apr 21, 2018
@weswigham
Copy link
Member Author

I've closed this PR, since this is a little too complex for most uses, and doesn't help with the named-argument-bag case that named arguments also are desired for. Hopefully we'll have a candidate ready for named type arguments available soon(tm).

@alvis
Copy link

alvis commented Apr 21, 2018

That's very sad @weswigham. Many users, including myself, have been watching this WIP and anticipated much for its coming. IMHO, it is one of the most wanted features in Typescript.

@weswigham
Copy link
Member Author

weswigham commented Apr 21, 2018

What's your practical use for it that can't also be solved with named type arguments and automatically inferring all missing type arguments?

@alvis
Copy link

alvis commented Apr 21, 2018

In Redux for instance, for creating a type-safe action, you really wish to have an inferred type e.g.

interface FSA<A,P>{
  type: A;
  payload: P;
}

function actionADD<infer A, infer P>(amount: number): FSA<A, P> {
  return {
    type: "ADD",
    payload: amount
  };
}

Here actionADD should have an inferred return type of FSA<'ADD', number>.

With typesafe-actions, we have achieved something, but it's still by far not ideal. At the moment, to achieve the resulting type above, we use

const actionADD = createAction('ADD', (amount: number) => ({
    type: 'ADD',
    payload: amount,
  })),
}

Obviously, it is not the way an action function is usually written in js. typesafe-actions provides a type-safe option, but it also creates a new syntax for the sake of being type safe. Also, even with typesafe-actions, noticed that it still needs an extra parameter 'ADD' in the function. The existence of the parameter is purely due to the inability of referring the 'ADD' type from the returned object.

IMO, users should not be required to change the syntax dramatically from the js world and yet they can still enjoy the benefit of typescript because types should be able to be automatically inferred. The need of much refactoring is a huge barrier stopping people to use typescript.

@masaeedu
Copy link
Contributor

Here actionADD should have an inferred return type of FSA<'ADD', number>

If you just remove all the type annotations, actionADD will have a return type of { type: "ADD", payload: number }, which is assignable wherever an FSA<A, P> is expected.

interface FSA<A,P>{
  type: A;
  payload: P;
}

function actionADD(amount: number) {
  return {
    type: "ADD",
    payload: amount
  };
}

function fsaPayload<A, P>(fsa: FSA<A, P>) {
  return fsa.payload
}
function fsaType<A, P>(fsa: FSA<A, P>) {
  return fsa.type
}


fsaPayload(actionADD(10)) // number
fsaType(actionADD(10)) // string

Type parameters on functions are for creating polymorphic functions, i.e. functions for which some parameter(s) may assume any type subject to constraints. We don't really need them if we're writing a function with a completely fixed type.

@MartinJohns
Copy link
Contributor

If you just remove all the type annotations, actionADD will have a return type of { type: "ADD", payload: number },

This is not correct. The return type will be { type: string, payload: number }.

@masaeedu
Copy link
Contributor

masaeedu commented Apr 21, 2018

@MartinJohns Oh, right. See #20195. This would be an issue in the OP's original example as well, since { type: "ADD" } is inferred as the widened type { type: string }.

In the meantime, you have to do:

function actionADD(amount: number) {
  return {
    type: "ADD" as "ADD",
    payload: amount
  };
}

@MeirionHughes
Copy link

MeirionHughes commented Apr 21, 2018

@weswigham See #22368 (comment)

the fleshed out example would be :

app.use(<infer T>(ctx: T & {required: true}, next: (_ctx : T & {required: true, extra: string})=>void)=>void );

currently I can do...

app.use((ctx, next: (_ctx : typeof ctx & { extra: string} ) =>void))=>{} );

here ctx is automatically inferred and the subsequent inference of _ctx (by app.use) is correct.

I think this PR and its explicit inference is the only way to solve the use-case where you want to infer a type and use it in a union/intersection: i.e. <infer T>(ctx: T & {required: true}...

@alvis
Copy link

alvis commented Apr 22, 2018

@masaeedu Thanks for your comment, but there's a problem I can see, which is not being able to check the validity of the structure of the return. In the example, I want to make sure actionADD returns a valid FSA structure. Without the type annotation, you can only check via other means. Also, a workaround for this typescript problem is to remove type annotations, isn't it ironic?

@unional
Copy link
Contributor

unional commented Feb 5, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

'infer' type parameters declarations aren't printed in quick info.