Skip to content

[isolatedDeclarations][5.5] Adding satisfies to a const expression makes it require explicit type annotation #58397

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

Closed
MichaelMitchell-at opened this issue May 1, 2024 · 11 comments

Comments

@MichaelMitchell-at
Copy link
Contributor

πŸ”Ž Search Terms

isolated declarations, satisfies, const

πŸ•— Version & Regression Information

This changed in commit or PR #58201

⏯ Playground Link

https://tsplay.dev/wRbGEw

πŸ’» Code

export const foo = {
  a: 42
} as const satisfies Record<string, number>;

πŸ™ Actual behavior

Variable must have an explicit type annotation with --isolatedDeclarations.ts(9010)

πŸ™‚ Expected behavior

No errors

Additional information about the issue

No response

@jakebailey
Copy link
Member

My impression was that satisfies may impact the contextual type of an expression and cause its type to change, such that its type may not be syntactically inferable. (Need to dig up that example again, though.)

@MichaelMitchell-at
Copy link
Contributor Author

My impression was that satisfies may impact the contextual type of an expression and cause its type to change, such that its type may not be syntactically inferable. (Need to dig up that example again, though.)

Yep I've definitely seen cases where satisfies influences the contextual type of an expression, but I wonder if the as const + direct assignment to a variable is a sufficiently well-defined scenario that we can always trivially extract the type.

@dragomirtitian
Copy link
Contributor

It is not unfortunately:

const x = [function() { return "A"}] as const satisfies Array<() => "A">
const x = [function() { return "A"}] as const

Playground Link

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label May 2, 2024
@MichaelMitchell-at
Copy link
Contributor Author

MichaelMitchell-at commented May 2, 2024

I wonder if the problem boils down to it not being possible to determine whether the const inference of an expression or the satisfies inference or combination of the two leads to the more specific type, then can we defer that decision down the line, so that

export const x = [function() { return "A"}] as const satisfies Array<() => "A">

would be emitted as something like

export declare const x: MakeSatisfy<readonly [() => string], [() => "A"]>;

Are there cases where knowing only the const inference of an expression + the satisfies type constraint isn't sufficient to determine the contextually inferred type?

@MichaelMitchell-at
Copy link
Contributor Author

MichaelMitchell-at commented May 2, 2024

Forgive me for pushing on this, as I'm sure folks are eager to treat this as an open and shut case. We have hundreds of instances of this pattern in our code base, the majority in which are cases where satisfies wouldn't affect the contextually inferred type. We adopted the operator early on under a different assumption

The new satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression[0]

So while we could migrate back to our old pattern (since duplicating the structure of the expression with an explicit type is the less desirable option) of doing a type test like

function upcast<T>(value: T): void { return value }

export const foo = {
  a: 42
} as const;
upcast<Record<string, number>>(foo);

asking developers to write unidiomatic code with an abstruse explanation is an outcome I want to push against.

[0] https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#satisfies

@fatcerberus
Copy link

satisfies invoking contextual typing is a necessary evil imo; you want e.g. [42] satisfies [number] to succeed, but for that you need to contextually type [42] by [number] so that it doesn't widen to an array type before it can be checked. But that in turn means that [42] ultimately gets inferred as a tuple type instead of an array type. There's no mechanism to "reverse" that after-the-fact because it actually changes which type is initially inferred for the expression.

@MichaelMitchell-at
Copy link
Contributor Author

@fatcerberus I get that, I'm not looking for a rationalization of the behavior

@RyanCavanaugh RyanCavanaugh removed the Working as Intended The behavior described is the intended behavior; this is not a bug label May 2, 2024
@jakebailey
Copy link
Member

jakebailey commented May 2, 2024

There's a general class of "operations that non-trivially affect the actual type", including but not limited to:

  • satisfies
  • ... ? ... : ...
  • Some forms of destructuring + defualting
  • basically anything with widening / freshness (including Symbol, unfortunately)
  • class expressions

Isolated declarations is designed to be syntax-only, meaning an external implementation (or TS internally) must always be able to detect when there's a problem or produce an equivalent annotation / initializer.

If we do want to allow these things, then dts emit is going to have to gain some sort of syntax or builin type to describe these behaviors, but right now, they don't really exist.

@RyanCavanaugh
Copy link
Member

If the satisfies operator doesn't change the type of the inferred expression, then it's always safe to rewrite this into two lines:

export const foo = {
  a: 42
} as const;
foo satisfies Record<string, number>;

@MichaelMitchell-at
Copy link
Contributor Author

MichaelMitchell-at commented May 2, 2024

If the satisfies operator doesn't change the type of the inferred expression, then it's always safe to rewrite this into two lines:

export const foo = {
  a: 42
} as const;
foo satisfies Record<string, number>;

While visually will take some getting used to, I suppose that's a good enough solution (thank you for taking the time to think of and provide one and not just mansplaining how things work).

@fatcerberus
Copy link

thank you for providing one and not just mansplaining how things work

"Mansplaining" assumes bad faith; it isn't meant to be patronizing. Generally it's been my experience that when you tell people there's a design limitation preventing a desired behavior, they tend to say "well why can't you just do/not do this one thing instead", so it's easier just to explain how things interconnect upfront.

Also why is "mansplaining" even a word

@microsoft microsoft locked as resolved and limited conversation to collaborators May 2, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants