X Tutup
The Wayback Machine - https://web.archive.org/web/20200916193416/https://github.com/Microsoft/TypeScript/pull/26349
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

Implement partial type argument inference using the _ sigil #26349

Open
wants to merge 9 commits into
base: master
from

Conversation

@weswigham
Copy link
Member

weswigham commented Aug 10, 2018

In this PR, we allow the _ sigil to appear in type argument lists in expression positions as a placeholder for locations where you would like inference to occur:

const instance = new Foo<_, string>(0, "");
const result = foo<_, string>(0, "");
const tagged = tag<_, string>`tags ${12} ${""}`;
const jsx = <Component<_, string> x={12} y="" cb={props => void (props.x.toFixed() + props.y.toUpperCase())} />;

This allows users to override a variable in a list of defaulted ones without actually explicitly providing the rest or allow a type variable to be inferred from another provided one.

Implements #26242.
Supersedes #23696.

Fixes #20122.
Fixes #10571.

Technically, this prevents you from passing a type named _ as a type argument (we do not reserve _ in general and don't think we need to). Our suggested workaround is simply to rename or alias the type you wish to pass. Eg,

interface _ { (): UnderscoreStatic; }

foo<_>(); // bad - triggers partial type inference, instead:

type Underscore = _;
foo<Underscore>(); // good

we did a quick check over at big ts query, and didn't find any public projects which passed a type named _ as a type argument in an expression/inference position, so it seems like a relatively safe care-out to make.

Prior work for the _ sigil for partial inference includes flow and f#, so it should end up being pretty familiar.

@alfaproject
Copy link

alfaproject commented Aug 10, 2018

Why write infer explicitly? Could we do like destructuring? <, string> instead of <infer, string>

By default, it would infer.

@weswigham
Copy link
Member Author

weswigham commented Aug 10, 2018

@alfaproject I wrote my rationale down in #26242

@jwbay
Copy link
Contributor

jwbay commented Aug 14, 2018

Would this PR enable this scenario? I didn't see a test quite like it. Basically extracting an inferred type parameter from a specified type parameter.

type Box<T> = { value: T };

type HasBoxedNumber = Box<number>;

declare function foo<T extends Box<S>, S>(arg: T): S;

declare const hbn: HasBoxedNumber;

foo<HasBoxedNumber, infer>(hbn).toFixed();
weswigham added 3 commits Aug 17, 2018
… fails)
@weswigham
Copy link
Member Author

weswigham commented Aug 17, 2018

Based on the design meeting feedback, this has been swapped to variant 2 from the proposal - using the * sigil as a placeholder for inference. We'll need updates to our tmlanguage to get syntax highlighting right (although we already parsed * in type positions for jsdoc, so we probably should have already).

sheetalkamat added a commit to microsoft/TypeScript-TmLanguage that referenced this pull request Aug 17, 2018
@weswigham
Copy link
Member Author

weswigham commented Aug 17, 2018

Would this PR enable this scenario? I didn't see a test quite like it. Basically extracting an inferred type parameter from a specified type parameter.

As is, no. Other type parameters (supplied or no) are not currently inference sites for a type parameter. We could enable it here (just by performing some extra inferType calls between the supplied types and their parameters' constraints), probably, but... should we? @ahejlsberg you have an opinion here?

@ahejlsberg
Copy link
Member

ahejlsberg commented Aug 17, 2018

@ahejlsberg you have an opinion here?

I don't think we want constraints to be inference sites, at least not without some explicit indication. At some point we might consider allowing infer declarations in type parameter lists just as we do in conditional types:

type Unbox<T extends Box<infer U>> = U;

Though you can get pretty much the same effect with conditional types:

type Unbox<T extends Box<any>> = T extends Box<infer U> ? U : never;
@weswigham
Copy link
Member Author

weswigham commented Aug 17, 2018

Alright, I'll leave this as is then and just mention that it's available as a branch if we ever change our minds in the future.

@weswigham weswigham changed the title Implement partial type argument inference using the infer keyword Implement partial type argument inference using the * sigil Aug 17, 2018
@treybrisbane
Copy link

treybrisbane commented Aug 18, 2018

@weswigham It seems inconsistent (and kinda strange) to use the * sigil for this when we already use the infer keyword to denote explicit type inference...

type Tagged<O extends object, T> = O & { __tag: T };

// "Infer a type, and make it available under the alias 'T'"
declare function getTag<O extends Tagged<any, any>>(object: O): O extends Tagged<any, infer T> ? T : never;

// "Infer a type, and make it available to 'getTag' under the alias at the first type position"
getTag<infer>({ foo: string, __tag: 'bar' })
// => 'bar'

This seems like an obvious syntactic duality to me... What was the reason you instead decided to go with *?

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Aug 21, 2018

The existing infer T keyword produces a new binding for T; this wouldn't be available in argument positions (e.g. you can't write getFoo<infer T, T>()). Having the infer keyword have arity 1 in conditional types and arity 0 in type argument positions seems like a decrease in overall consistency rather than an increase.

@KyleDavidE
Copy link

KyleDavidE commented Aug 22, 2018

It would probably be nice to be able to declare infer on the functions, ex: function foo<A, B = infer>(b: B, c: SomeComplexType<A,B>): SomeOtherComplexType<A,B>

@treybrisbane
Copy link

treybrisbane commented Aug 23, 2018

@RyanCavanaugh

Having the infer keyword have arity 1 in conditional types and arity 0 in type argument positions seems like a decrease in overall consistency rather than an increase.

Thanks for the response. :)

Fair enough, but I'd argue that this decrease in consistency is far less than that of introducing an entirely new sigil for this purpose. Is there really a benefit to users in using such a radically different syntax for something whose only difference to infer T is the arity?

@treybrisbane
Copy link

treybrisbane commented Aug 23, 2018

Something else to consider is that TypeScript supports JSDoc, and * in JSDoc means any. I'm not sure it's a good idea to reuse a symbol that means any in one context for something that means "please infer this type for me" in another context.

If we're concerned about making operators/keywords context-sensitive, then again it seems like making infer context-sensitive is far less of an evil than doing the same for *.

@insidewhy
Copy link

insidewhy commented Aug 31, 2018

I don't mind * as it jives with flow. Users of typescript can just avoid * in jsdoc and always use any for the purpose easily enough?

I'd also like to see this:

const instance = new Blah<T, **>(1, 'b', false, new Date())

I have a class that bundles many string literal types and I have to enumerate them all at every callsite even when I'm using the code from this branch. Everytime I add a new string literal I have to update every single callsite which is a massive drag ;)

@insidewhy
Copy link

insidewhy commented Sep 1, 2018

Consider:

type LiteralMap<S1 extends string, S2 extends string, S3 extends string> = {
  item1: S1,
  item2: S2,
  item3: S3
}

With this feature at every definition using this type I have to use:

function user(map: LiteralMap<*, *, *>) {}

Now if I need to add a new literal to my map I have to update this to:

type LiteralMap<S1 extends string, S2 extends string, S3 extends string, S4 extends string> = {
  item1: S1,
  item2: S2,
  item3: S3,
  item4: S4,
}

which is no big deal, but now I also have to update every single use of this to:

function user(map: LiteralMap<*, *, *, *>) {}

With LiteralMap<**> I can just update the definition without affecting every area it is used.

@svieira svieira mentioned this pull request Sep 1, 2018
4 of 4 tasks complete
@xaviergonz
Copy link

xaviergonz commented Sep 1, 2018

Or it could follow the tuple system

type LiteralMap<S1?, S2?, S3?> = {
  item1: S1,
  item2: S2,
  item3: S3
}

function user(map: LiteralMap) {} // infer, infer, infer
function user(map: LiteralMap<boolean>) {} // boolean, infer, infer
function user(map: LiteralMap<_, boolean>) {} // infer, boolean, infer

type LiteralMap<S1, S2, S3?> = {
  item1: S1,
  item2: S2,
  item3: S3
}

function user(map: LiteralMap) {} // not allowed, S1 and S2 missing
function user(map: LiteralMap<boolean>) {} // not allowed, S2 missing
function user(map: LiteralMap<_, boolean>) {} // infer, boolean, infer

alternatively it could use the default assignation (which I guess makes more sense, since if you want it to infer the default type makes no sense?)

type LiteralMap<S1 = _, S2 = _, S3 = _> = {
  item1: S1,
  item2: S2,
  item3: S3
}

function user(map: LiteralMap) {} // infer, infer, infer
function user(map: LiteralMap<boolean>) {} // boolean, infer, infer
function user(map: LiteralMap<*, boolean>) {} // infer, boolean, infer

type LiteralMap<S1, S2, S3 = _> = {
  item1: S1,
  item2: S2,
  item3: S3
}

function user(map: LiteralMap) {} // not allowed, S1 and S2 missing
function user(map: LiteralMap<boolean>) {} // not allowed, S2 missing
function user(map: LiteralMap<_, boolean>) {} // infer, boolean, infer
@nickbclifford
Copy link

nickbclifford commented Jun 29, 2019

What's blocking this from being merged right now?

@trusktr
Copy link

trusktr commented Jul 14, 2019

This will be useful in making mixin class definitions more terse!

examples

I wanted to do the following, but then I discovered that supplying only the first generic arg to a generic type causes the remaining args not to be inferred:

function AwesomeMixin<T extends Constructor>(Base: T) {
    // This would be nice, a properly typed mixin all in a single return statement.
    // Pretend the second arg in the following MixinResult application is inferred:
    return MixinResult<T>(class Awesome extends Constructor(Base) {
        // ...
    })
}

type Constructor<T = object, A extends any[] = any[]> = new (...a: A) => T

function Constructor<T = object, Static = {}>(Ctor: Constructor<any>) {
    return (Ctor as unknown) as Constructor<T> & Static
}

function MixinResult<TBase extends Constructor, TClass extends Constructor = Constructor>(Class: TClass) {
    return Class as Constructor<InstanceType<TClass> & InstanceType<TBase>> & TClass & TBase
}

Note, you may think that you can still have a single-return mixin and don't need to do what I've done above, but if you try composing multiple mixins, then you'll run into issues in #32080.

I need to compose mixins like the following:

// a mixin for use on Custom Elements, and composed of multiple mixins:
function AwesomeMixin<T extends Constructor<HTMLElement>>(Base: T) {
    // Pretend the second arg in the following MixinResult application is inferred:
    return MixinResult<T>(class Awesome extends Mixin1(Mixin2(Constructor<HTMLElement>(Base))) {
        // ... inside here, we get all types from Mixin1, Nixin2, and HTMLElement ...
    })
}

where Mixin1 and Mixin2 are defined using the same patterns, and may also be composed from yet other mixins.

But without some new feature from #32080, writing just Base instead of Constructor<HTMLElement>(Base) will result in HTMLElement types not being available inside the mixin class body (errors trying to use HTMLElement props/methods).

For the time being, I have to resort to the slightly less terse version due to current lack of inference on generic args when only some are supplied:

function AwesomeMixin<T extends Constructor<HTMLElement>>(Base: T) {
    class Awesome extends Mixin1(Mixin2(Constructor<HTMLElement>(Base))) {
        // ...
    }
    // less terse, more duplication of the Awesome identifier
    return Awesome as MixinResult<typeof Awesome, T>
}


// ... the other parts omitted ... and MixinResult is only a type, not a function:
type MixinResult<TClass extends Constructor, TBase extends Constructor> =
    Constructor<InstanceType<TClass> & InstanceType<TBase>> & TClass & TBase

And even if we had the inferred elided args in the future, but you still wanted the return to be a separate statement, then the benefit of this feature would still be nice:

function AwesomeMixin<T extends Constructor<HTMLElement>>(Base: T) {
    class Awesome extends Mixin1(Mixin2(Constructor<HTMLElement>(Base))) {
        // ...
    }
    // better, one less "Awesome" identifier than the previous example
    return MixinResult<T>(Awesome)
}

// ... other stuff omitted ... but MixinResult is a function again:
function MixinResult<TBase extends Constructor, TClass extends Constructor = Constructor>(Class: TClass) {
    return Class as Constructor<InstanceType<TClass> & InstanceType<TBase>> & TClass & TBase
}
@fwh1990
Copy link

fwh1990 commented Oct 14, 2019

Currently it's most expected feature in TypeScript for me 😵

I really need this feature right now.

@szszoke
Copy link

szszoke commented Nov 22, 2019

Could this support a scenario where the inference of a generic parameter would be the default behavior?

I'm thinking of something like this:

function f<P = never, T extends string = _>(type: T) {}

This would be useful for Redux action creators.

It would allow us to have an optional generic parameter but still have type infered as a string literal type. Then we could have a conditional redurn type that would depend on P being never or something else.

@dhoulb
Copy link

dhoulb commented Nov 23, 2019

Just adding some design thoughts :)

Use the infer keyword over _ underscore or * asterisk

I really think this would be a good choice for the following reasons:

  1. It's more understandable at-a-glance. A new TS developer can guess what it's doing from a plain reading, whereas they'd definitely have to Google for documentation on _ underscore (and it's hard to Google punctuation). While * asterisk is better I still think the first thought most devs would have is that it's an alternate for any. Using infer doesn't have this problem.

  2. Fewer special characters. TS already looks a bit scrappy with lots of brackets and punctuation going on. Consider how much cleaner the myVar as string is than using <string>myVar

  3. Less future confusion. You can imagine an asterisk or underscore being used in JavaScript in the next five years to mean something else (e.g. private properties?) which could end up being confusing.

Use infer as default type in generic declaration

@KyleDavidE suggested something above that I loved, and that goes great with this thread: being able set the default value of a generic to infer, so you can partially specify generics without having to fill infer from the right too when you're using the type.

It would look something like this:

function getName<A, B = infer>(a: A, b: B): B {}

const abc: boolean = getName<string>("abc", true);
@jesseoh
Copy link

jesseoh commented Dec 7, 2019

@Nandiin If you modify your first workaround example a bit, the end usage isn't really all that bad. You still have multiple parentheses, but at least the generic parameters aren't awkwardly sandwiched between them:

type Modified<O, K extends string, T> = O & Record<K, (v: T) => void> \n
declare function modify<T>(): <O, K extends string>(target: O, k: K) => Modified<O, K, T>
const John = {}
const modified = modify<number>()(John, 'age')
modified.age // (v: number) => void
modified.age(18)`
@Ranguna
Copy link

Ranguna commented Dec 15, 2019

@weswigham You are incredible, great work!

I'm patiently waiting for this merge.
Currently I'm using the currying workaround which I'd very much like to stop using.

Is there any ETA on this merge ?

@Losses
Copy link

Losses commented Jan 15, 2020

Yeah... currying looks so dirty... hope this PR could be merged soon...

@alvis
Copy link

alvis commented Apr 16, 2020

Like @dhoulb's idea on using the infer keyword for inferring a type. Just a slight modification suggestion:

type TypeConstraint = string | boolean;
function getName<A, infer B extends TypeConstraint>(a: A, b: B): B {}

// ok
const abc: boolean = getName<string>("abc", true);
const abc: boolean = getName<string, boolean>("abc", true);

// error
const abc: boolean = getName<string>("abc", 10);
@satanTime
Copy link

satanTime commented May 10, 2020

Should someone overtake this? I see last changes in 2018, but this feature would be really cool

@weswigham
Copy link
Member Author

weswigham commented May 10, 2020

This is not stalled for lack of ownership, rather, for lack of drive to accept the feature in the first place.

@satanTime
Copy link

satanTime commented May 10, 2020

I see, sad sad, I would vote for <infer, string> or <, string>, but _ looks strange IMO.

@zachkirsch
Copy link

zachkirsch commented May 10, 2020

This is not stalled for lack of ownership, rather, for lack of drive to accept the feature in the first place.

@DanielRosenwasser @ahejlsberg @RyanCavanaugh @andy-ms as the reviewers on this PR, can you provide an update as to why this PR isn't being accepted? There seems to a lot of interest from the community in this feature.

@insidewhy
Copy link

insidewhy commented May 11, 2020

The sad thing about this is it's a problem I hit with almost every typescript project I work on.

And the only way around it is to add an extra function. So in turn my APIs stop making sense (especially for javascript consumers) because now people have to use doStuff(1)(2, 3) when they should just be able to use doStuff(1, 2, 3).

@kotarella1110
Copy link

kotarella1110 commented May 11, 2020

I thought this would be accepted in the near future as it was added to the roadmap.
But this is no progress... Why not merge?

https://github.com/microsoft/TypeScript/wiki/Roadmap

スクリーンショット 2020-05-11 17 01 25

@awerlogus
Copy link

awerlogus commented May 13, 2020

@weswigham Maybe the better way is to implement this proposal?

If you pass the first type parameter to myLookerUpper2 function, the second type parameter will be automatically inferred as default type assigned to it — keyof T. To improve type inference we can use default types only in cases when type parameter can't be inferred another way. So, it means that in case of this function

declare function myLookerUpper2<T, K extends keyof T = keyof T>(t: K): T[K]

function parameter t should have higher priority to infer type parameter K than default type parameter value keyof T

Then we will be available to move all type parameters that should be inferred automatically to the end of the list and assign them all default values.

This approach has some pros:

  1. No need to add all that over-engineering with placeholders.
  2. Developer can choose by his own the default type for the case if type parameter can't be inferred automatically.
  3. Simple to implement (I guess)
@trusktr
Copy link

trusktr commented May 13, 2020

@awerlogus but there will still be a case of someone wanting to specify a specific type arg in the middle of the inferrable parameters. That proposal I don't think covers that case like this one does.

I also like infer better. The docs need to clearly explain the arity differences depending on usage site.

Plus I think it makes things like the following more sensible and inline with familiar patterns:

Instead of

const instance = new Blah<T, **>(1, 'b', false, new Date())

we can write

const instance = new Blah<T, ...infer>(1, 'b', false, new Date())

But arguably with trailing parameters that should just be

const instance = new Blah<T>(1, 'b', false, new Date())

but the only bad thing is that it may be a breaking change. :/ In that case, ...infer is inline with familiar patterns.

For non-trailing args, to avoid writing infer many times, what about something like

const instance = new Blah<infer * 2, T, infer>(1, 'b', false, new Date())

where the *2 spreads the infer across two parameters?

@AustP
Copy link

AustP commented Jun 21, 2020

I was looking for this functionality in TypeScript as well and I spent a few hours trying to get this working. During my exploration I naturally discovered the infer and leading comma syntaxes:

type S = {
  someProperty: string;
};

// infer syntax
function f1<T = never, U extends keyof S = infer>(param: U): U | T {
  return param;
}

const a1 = f1<boolean>('someProperty');

// leading comma syntax
function f2<U extends keyof S, T = never>(param: U): U | T {
  return param;
}

const b1 = f2<, boolean>('someProperty');

Although I'd be tickled pink if this PR got merged with the current syntax, I would prefer one of these alternative syntaxes. They have both been proposed already in this thread and I think it is worth noting that I discovered them both naturally before even seeing the proposals. On the other hand, I did not naturally discover the underscore syntax which is currently the syntax of the PR. To me that says that the underscore syntax should be replaced with one of the other two, but that's just my two cents. If I could choose, I would choose the infer syntax over the others.

@MrLoh
Copy link

MrLoh commented Aug 2, 2020

It would be great to see this merged, no matter what syntax is used.

@jamiepine
Copy link

jamiepine commented Aug 6, 2020

Also bumping this, would love the keyword "infer". Seems clean and verbose.

@YunHsiao
Copy link

YunHsiao commented Sep 1, 2020

This is... interesting. If it's for compatibility concerns can't we just set a new compiler flag on this and get on with it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
X Tutup