Skip to content

feat(7481): Operator to ensure an expression is contextually typed by, and satisfies, some type #46827

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

Merged
merged 19 commits into from
Aug 26, 2022

Conversation

a-tarasyuk
Copy link
Contributor

@a-tarasyuk a-tarasyuk commented Nov 16, 2021

Fixes #7481
Fixes #47920

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Nov 16, 2021
@RyanCavanaugh
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 16, 2021

Heya @RyanCavanaugh, I've started to run the tarball bundle task on this PR at 37dd828. You can monitor the build here.

@RyanCavanaugh
Copy link
Member

Let's try again

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 16, 2021

Heya @RyanCavanaugh, I've started to run the tarball bundle task on this PR at 37dd828. You can monitor the build here.

@andrewbranch
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 16, 2021

Heya @andrewbranch, I've started to run the tarball bundle task on this PR at 37dd828. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 16, 2021

Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/115177/artifacts?artifactName=tgz&fileId=0FF88EE1B199729611A6853F28CD3BDC2D490E9405534DBAA35BB52AA8A7F03102&fileName=/typescript-4.6.0-insiders.20211116.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@typescript-bot typescript-bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Nov 17, 2021
@a-tarasyuk
Copy link
Contributor Author

@DanielRosenwasser @RyanCavanaugh Thanks for the feedback. I've added the requested changes.

@RyanCavanaugh
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 17, 2021

Heya @RyanCavanaugh, I've started to run the tarball bundle task on this PR at 37d57da. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 17, 2021

Hey @RyanCavanaugh, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/115217/artifacts?artifactName=tgz&fileId=2480AE62D0684243B34156861C597CC60304449F7DBB697789A40B55586144C602&fileName=/typescript-4.6.0-insiders.20211117.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@rbuckton
Copy link
Member

I'd rather we avoid further polluting JS expression space with new expression-level operators. Could we achieve this with a magic type similar to ThisType<T>?

// NOTE: Follows new mechanism for defining magic types like `Uppercase<T>` 
// as opposed to `ThisType<T>`
type Satisfies<T> = intrinsic; 
 
const options = { } as Satisfies<Options>;

@andrewbranch
Copy link
Member

I find the postfix operation nice for the “ensure this literal I wrote in the middle of some expression is assignable to T,” but very awkward for flowing contextual types into the expression, which is a case I find more compelling on the whole. Consider

const actions = {
  foo: payload => {
    // ...
  },
  bar: payload => {
    // ...
  },
  baz: payload => {
    // ...
  },
  one: payload => {
    // ...
  },
  fish: payload => {
    // ...
  },
} satisfies Record<string, (payload: Whatever) => Promise<boolean>>;

You had to do a lot of reading before finding out what each payload parameter was supposed to be, and I omitted the body of each function. Even when you get there, the flowing lexically backwards of the contextual type feels unexpected to me. Of the syntax proposals I’ve seen, I kind of like as extends T because it feels like it could naturally be extended to variable declarations:

const actions: extends Record<string, (payload: Whatever) => Promise<boolean>> = {
  // ...
}

That does have the issue that an extends T annotation is meaningless in absence of an initializer and would probably have to be a grammar error, so it’s far from perfect, but having some way of writing the contextual type before its expression would feel much more ergonomic to me.

@RyanCavanaugh
Copy link
Member

I gave a lot of examples involving const x: T = ...-like constructs but I think a lot of the use cases here are in expression positions where there's no place to put a type annotation (e.g. function argument). Anything we do here should be useable in any expression position IMO

@awerlogus
Copy link

What about jsdoc compatibility?

@ChiriVulpes
Copy link
Contributor

ChiriVulpes commented Dec 9, 2021

I kind of like as extends T because it feels like it could naturally be extended to variable declarations: [...] having some way of writing the contextual type before its expression would feel much more ergonomic to me.

I gave a lot of examples involving const x: T = ...-like constructs but I think a lot of the use cases here are in expression positions where there's no place to put a type annotation (e.g. function argument). Anything we do here should be useable in any expression position IMO

Considering that this syntax is filling a similar role as a type assertion, it makes sense to me that whatever keyword is used can be positioned both as a postfix and part of declaring the type of a variable, to mirror type assertions. And, like... couldn't both positions be supported with either of these keywords? Like, all the the below examples seem equally sensible to me in reading them. Am I misunderstanding why not all of these examples could work, syntactically?

const foo: satisfies Bar = { ... };
applyBar({ ... } satisfies Bar);
const foo: extends Bar = { ... };
applyBar({ ... } as extends Bar);

Note that I personally cast no vote about which syntax to use, as I like the sound of "satisfies", and "extends" is kinda all over the place in TypeScript already, filling a ton of different roles, which can make code a little bit slower to comprehend imo. But at the same time, being able to use as in the postfix for this mirrors type assertions better, so... No preference here. The only camp I'm in is the camp of having both a type declaration and a postfix version of this operator. 😜

@danvk
Copy link
Contributor

danvk commented Dec 11, 2021

Whatever the syntax winds up being, it would be great if it could also be used to give a contextual type to JSON imports (#37831):

import data from './data.json' satisfies MyType;

@treybrisbane
Copy link

Got a question on this.

Given

interface Foo {
  bar: 1 | 2;
}

const value = { bar: 1 as const } satisfies Foo;

What is the type of value?

@ChiriVulpes
Copy link
Contributor

ChiriVulpes commented Dec 12, 2021

@treybrisbane

What is the type of value?

{ bar: 1 }

It's not a postfix to change the type, it's a postfix to validate the type. It's making sure whatever value is there has a type which, in this case, is assignable to Foo

*I'm pretty sure there's some other stuff going on with like, turning parts of the value's type into the type in satisfies via contextual typing, most likely exclusively in cases that don't widen it. I might be wrong on that front though? That's stuff I don't really understand besides how to use it haha

@andrewbranch
Copy link
Member

It was discussed in a design meeting (#50522), but I don’t think we had a conclusion:

  • JSDoc?
    • We try not to add new tags that aren't doc'd in the usejsdoc site.
    • Ship has sailed with TS-specific syntax in JSDoc, right?

@ChiriVulpes
Copy link
Contributor

@a-tarasyuk @andrewbranch Is the team against doing something like /** @type {satisfies Interface} */(value) ? (I'll post this again in a new issue when it's created, if it could be done)

@brendankenny brendankenny mentioned this pull request Oct 6, 2022
5 tasks
@brendankenny
Copy link
Contributor

brendankenny commented Oct 6, 2022

Opened #51086 for satisfies in jsdoc discussion

@Andarist
Copy link
Contributor

Andarist commented Oct 12, 2022

I wonder if it was a deliberate decision to preserve the "original" type of expression for expressions that are not satisfied by the operator (in case of an error). This is different from the as operator:
TS playground

@RyanCavanaugh
Copy link
Member

We didn't discuss what to do in error cases, oddly enough. I think keeping the expression type regardless of outcome is the most consistent/predictable behavior, even if the net behavior is maybe a little less desirable (since, presumably, you now have two errors in your program instead of one).

I think there's something weird going on with the language service displaying the asserted type in the Playground. The actual type of the variable, shown in .d.ts emit, is the expression type.

@DanielRosenwasser
Copy link
Member

There are 3 things I can imagine us doing:

  • The "error" type (this is an internal any)
  • The original type
  • The original type intersected with the expected type

I think the original type is fine to start with. I think the error type is possibly undesirable because you lose other semantic smarts you'd get in the editor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project