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

Contextually infer parameters for type aliases/interfaces #32794

Open
ethanresnick opened this issue Aug 9, 2019 · 7 comments

Comments

@ethanresnick
Copy link
Contributor

commented Aug 9, 2019

Search Terms

type alias parameter inference contextual

Suggestion

The type parameters of a type alias (or interface) should be able to be inferred when a value is being assigned/cast to that type/interface.

Use Cases/Examples

My program defines a few common types/interfaces to be used as contracts between components. E.g.

// type for an object holding a function + its inverse
type FunctionPair<T, U> = { apply(it: T): U, reverse(it: U): T }; 

Then, throughout the program, I need to make objects of this type. If I have a factory function (or use a class with its constructor), this isn't too bad:

function makeFunctionPair<T, U>(apply: (it: T) => U, reverse: (it: U) => T) {
  return { apply, reverse } as FunctionPair<T, U>;
}

However, I'd like to be able to just write these (simple) domain objects with literals, rather than using a factory function, and then signal the type (with the implied relation between the apply and reverse properties) to the compiler with a type annotation/assertion inline:

const a: FunctionPair<string, () => string> = {
    apply(it: string) { return () => it + "!!!"; },
    reverse(it) { return it().slice(0, -3); }
}

However, the above is a bit verbose, in that I have to add <string, () => string> to the type annotation, whereas it seems like this should be inferrable. I'm proposing to be able to just do:

 /* FunctionPair type param values inferred contextually from the assigned object  */
const a: FunctionPair = {
    apply(it: string) { return () => it + "!!!"; },
    reverse(it) { return it().slice(0, -3); }
}

Here's another example: imagine a runtime that uses the idea of effects as data. The user sends into the runtime an object describing the effect to perform, and a callback to call with any result:

type EffectNamesWithReturnTypes = { 
    x: { status: 0 | 1 },
    y: { changes: string[] }, 
};

type EffectDescriptor<T extends keyof EffectNamesWithReturnTypes> = { 
  name: T; 
  cb: (result: EffectNamesWithReturnTypes[T]) => void
}

It would be nice to be able to write:

const effect = <EffectDescriptor>{ name: "x", cb: (it) => it.status };

Rather than having to write:

const effect = <EffectDescriptor<"x">>{ name: "x", cb: (it) => it.status };

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@fatcerberus

This comment has been minimized.

Copy link

commented Aug 10, 2019

@ethanresnick

This comment has been minimized.

Copy link
Contributor Author

commented Aug 10, 2019

Thanks @fatcerberus. I saw that issue earlier, but my understanding is that it does something slightly different, namely: in a position where TS would either infer all the parameters or require you to specify all the parameters, #26349 would let the user specify some and have others be inferred. In the cases I'm talking about, though, it's not clear to me if TS has an inference mechanism for these parameters at all, which is why they all have to be specified.

@ethanresnick

This comment has been minimized.

Copy link
Contributor Author

commented Aug 13, 2019

@RyanCavanaugh Any thoughts about this idea?

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented Aug 13, 2019

To avoid this being a breaking change for code that relies on default type arguments, it'd probably need to be an opt-in syntax e.g. EffectDescriptor<infer>. But all the machinery exists already since this effectively happens during function call inference.

That said, the use of a type assertion position here seems somewhat problematic. You wouldn't want to miss a property in the target type, for example, but that could easily happen here. Having a function like this would accomplish the use case 100% with existing syntax without introducing the potential to accidently write a supertype of the intended type.

function makeEffect<T>(arg: EffectDescriptor<T>) { return arg; }

const effect = makeEffect({ name: "x", cb: it => it.status} );

It seems like you need a variant of #7481 to avoid the downcasting problem.

@ethanresnick

This comment has been minimized.

Copy link
Contributor Author

commented Aug 14, 2019

all the machinery exists already since this effectively happens during function call inference.

That's great to hear!

it'd probably need to be an opt-in syntax e.g. EffectDescriptor

Agreed. Is there value in considering this syntax alongside the syntax for #26349? Or is it probably fine to pick the syntax for #26349 first, on the assumption that it'll be easy to extend consistently to cover this case? (I see that quite a few people requested/expected that #26349 cover this.)

That said, the use of a type assertion position here seems somewhat problematic. You wouldn't want to miss a property in the target type, for example, but that could easily happen here... It seems like you need a variant of #7481 to avoid the downcasting problem.

Good point. And I would be a happy camper even if this feature only worked as an annotation on the variable's type, like in my FunctionPair example.

It just felt a bit inconsistent to me to support the inference in one place but not the other, and it kinda feels like programmer's job to make sure they're comfortable with a possible downcast, which feels orthogonal to whether inference should happen.

That said, I definitely would want whatever the final syntax is in #7481 to be compatible with the idea here.

@fatcerberus

This comment has been minimized.

Copy link

commented Aug 14, 2019

and it kinda feels like programmer's job to make sure they're comfortable with a possible downcast, which feels orthogonal to whether inference should happen.

Oh, definitely, but it would be rather frustrating to have to choose between automatic inference of type parameters and an unguarded downcast, exactly because the two concerns are orthogonal.

@ethanresnick

This comment has been minimized.

Copy link
Contributor Author

commented Aug 14, 2019

Oh, definitely, but it would be rather frustrating to have to choose between automatic inference of type parameters and an unguarded downcast, exactly because the two concerns are orthogonal.

Right. I'm not arguing for coupling the two together, which is why I proposed that you could use this inference feature in a possibly-downcasting assertion, or in a variable's type annotation (which won't downcast).

Examples:

// 1. Inference with no downcasting
// Gives error about missing cb parameter, as would happen 
// if you specified the value for the type param manually
const effect: EffectDescriptor<infer> = { name: "x" };

// 2. Inference with downcasting
const effect = <EffectDescriptor<infer>>({ name: "x" });

// 3. No inference, with and without downcasting
// These two forms are exactly exists today
const effect = <EffectDescriptor<"x">>{ name: "x" };
const effect: EffectDescriptor<"x"> = { name: "x" };

And, if a non-downcasting type assertion operator is introduced in the future (#7481), the idea is that it could also be used with and without inference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.