Skip to content

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

Closed

Description

πŸ”Ž 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

Activity

  1. jakebailey commented on May 2, 2024

    @jakebailey
    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.)

  2. MichaelMitchell-at commented on May 2, 2024

    @MichaelMitchell-at
    ContributorAuthor

    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.

  3. dragomirtitian commented on May 2, 2024

    @dragomirtitian
    Contributor

    It is not unfortunately:

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

    Playground Link

  4. MichaelMitchell-at commented on May 2, 2024

    @MichaelMitchell-at
    ContributorAuthor

    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?

  5. MichaelMitchell-at commented on May 2, 2024

    @MichaelMitchell-at
    ContributorAuthor

    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

  6. fatcerberus commented on May 2, 2024

    @fatcerberus

    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.

  7. MichaelMitchell-at commented on May 2, 2024

    @MichaelMitchell-at
    ContributorAuthor

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

  8. jakebailey commented on May 2, 2024

    @jakebailey
    Member

    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.

  9. RyanCavanaugh commented on May 2, 2024

    @RyanCavanaugh
    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>;
  10. MichaelMitchell-at commented on May 2, 2024

    @MichaelMitchell-at
    ContributorAuthor

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

  11. fatcerberus commented on May 2, 2024

    @fatcerberus

    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

  12. locked as resolved and limited conversation to collaborators on May 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions