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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member Author

weswigham commented Aug 10, 2018

@alfaproject I wrote my rationale down in #26242

@jwbay

This comment has been minimized.

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 some commits Aug 17, 2018

@weswigham

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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 *.

@alvis alvis referenced this pull request Aug 27, 2018

Merged

Typescript Definitions #112

@ohjames

This comment has been minimized.

Copy link

ohjames 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 ;)

@ohjames

This comment has been minimized.

Copy link

ohjames 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 referenced this pull request Sep 1, 2018

Closed

Named Generic Type Parameter #26834

4 of 4 tasks complete
@xaviergonz

This comment has been minimized.

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

weswigham added some commits Nov 2, 2018

@weswigham weswigham changed the title Implement partial type argument inference using the * sigil Implement partial type argument inference using the _ sigil Nov 2, 2018

@weswigham

This comment has been minimized.

Copy link
Member Author

weswigham commented Nov 2, 2018

This proposal has been updated to use _ as the sigil following discussion today and investigation of some prior (and concurrent) work in the area.

sheetalkamat added a commit to Microsoft/TypeScript-TmLanguage that referenced this pull request Nov 2, 2018

@arogg

This comment has been minimized.

Copy link

arogg commented Nov 6, 2018

Will this allow me to enforce an object with all values of one type and still do a keyof on the keys and get the custom key names?? Something like this:

const myobj : Record<_,MyValue> = {
/* .... */
}

type keys = keyof typeof myobj;
@weswigham

This comment has been minimized.

Copy link
Member Author

weswigham commented Nov 6, 2018

Will this allow me to enforce an object with all values of one type and still do a keyof on the keys and get the custom key names??

Not at present, no - as is, this only allows partial inference at places where we already do inference - the top level of expression type argument lists.

@michaeljota

This comment has been minimized.

Copy link

michaeljota commented Nov 6, 2018

Just to be sure, this is not an existential operator, right?

@cevek

This comment has been minimized.

Copy link

cevek commented Nov 8, 2018

And again, it moves to the unknown future release... :(

@fahadash

This comment has been minimized.

Copy link

fahadash commented Nov 19, 2018

Why can't we do the inference the way C# does it?

Foo(0, "test");

// is same as

Foo<int, string>(0, "test");

Why do we need underscores?

@sledorze

This comment has been minimized.

Copy link

sledorze commented Nov 19, 2018

@fahadash the key word is partial

@sonhanguyen

This comment has been minimized.

Copy link

sonhanguyen commented Dec 11, 2018

What was the reasoning for the change * -> _. I understand that it's familiar to people coming from, say, scala, but <*> is used in java so it's not like It'll be alien to everyone.

Put that aside, the thing that doesn't fly with me the most is that _ is not a reserved word. So in principle, it could be a name (like lodash can choose to export a type of that name even if we know they're not). Ideally, I'd prefer the sigil to be the same to the smart pipe's placeholder, if that's ever gonna happen. I don't know If I prefer the one they currently propose which is #, but it seems likely to be the choice moving forwards given it's already used in the private proposal. What I'm pretty certain is that the smart pipe or any other placeholder that's gonna come in the future will not use _ for compatibility reasons.

@weswigham

This comment has been minimized.

Copy link
Member Author

weswigham commented Dec 11, 2018

Put that aside, the thing that doesn't fly with me the most is that _ is not a reserved word.

We introduce new global type names and contextual keywords in type contexts pretty often. Provided we don't break many people, we're fine with it (and it's not like we'll be breaking expression usages). Plus, we got prior art in the JS space - flow added _ as the placeholder for partial inference.

Ideally, I'd prefer the sigil to be the same to the smart pipe's placeholder, if that's ever gonna happen.

Total aside, but I, personally, have a strong dis-preference to any pipe proposal that introduces a context variable. Powershell and other shell languages made me come to hate context vars.

@Jessidhia

This comment has been minimized.

Copy link
Contributor

Jessidhia commented Dec 11, 2018

this message brought to you by my $_;

@ssube ssube referenced this pull request Jan 2, 2019

Closed

collect or complete helper #96

9 of 9 tasks complete
@MaxGraey

This comment has been minimized.

Copy link

MaxGraey commented Jan 16, 2019

What about extending _ and also using for explicit casting to expected type? For example we need unsafe cast number to boolean like:

function assert(value: boolean, msg?: string) {
  if (!value) throw new Error(msg || 'assert failure');
}
const value = 1;
assert(value as any);

instead of that we might write this:

const value = 1;
assert(value as _);

And _ in this case try inferring to destination type of function argument and fallback to any if this not possible.

@voliva

This comment has been minimized.

Copy link

voliva commented Jan 17, 2019

@MaxGraey Although I can see what you mean, I think that's not a good example because atm if you try casting value to boolean TS will report an error (I get Conversion of type 'number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other), so in that case it would just cast to any, making it a bit hard to see an added value.
as _ just quickly becomes an alias of as any

This example might be a bit better:

interface Foo {
    value: number
}

function increment(foo: Foo) {
    foo.value++;
}

increment({
    value: 1,
    bar: 'bar'
}); // Fails: Object literal may only specify known properties, and 'bar' does not exist in type 'Foo'.

increment({
    value: 1,
    bar: 'bar'
} as Foo); // Workaround

const obj = {
    value: 1,
    bar: 'bar'
}
increment(obj); // Also works.

Link to playground

In this case, we can argue whether we can just cast it to _ instead, and the compiler should already know that you can cast this object to Foo, because it's what the parameter expects.

But then on this case, isn't it a bit weird that casting obj to Foo works when using the declared obj variable and it doesn't when inlining it? Maybe this should be thought as a different issue instead.

@freund17

This comment has been minimized.

Copy link

freund17 commented Jan 17, 2019

@MaxGraey @voliva
First of all, you are arguing an entirely different feature here...
You should open a new issue.

Secondly, you must remember: Casting a type simply tells the compiler "trust me on this".
I see no use for a feature to tell the compiler "trust me on this, this is the type you are expecting" (... as _).
Because we already have "trust me on this, this can be anything you need it to be" (... as any).
Which might be different in semantics, but is exactly equivalent in the outcome. (Accepting a parameter which originally would not have been accepted)

@ExE-Boss

This comment has been minimized.

Copy link

ExE-Boss commented Feb 13, 2019

I would also like to be able to omit all inferred types when unambiguous:

interface SpecialArray<T> extends Array<T> {}

let a: SpecialArray<_> = [1]
// and
let b: SpecialArray<> = [1]
// are both equivalent to:
let c: SpecialArray<number> = [1];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment