microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.59k stars 12.43k forks source link

Contextually infer parameters for type aliases/interfaces #32794

Open ethanresnick opened 5 years ago

ethanresnick commented 5 years ago

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:

fatcerberus commented 5 years ago

https://github.com/microsoft/TypeScript/pull/26349

ethanresnick commented 5 years ago

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 commented 5 years ago

@RyanCavanaugh Any thoughts about this idea?

RyanCavanaugh commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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.