Skip to content

Enhance type predicates / Enhancing typescripts ability to handle state #50136

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
5 tasks done
TrevTheDev opened this issue Aug 2, 2022 · 4 comments
Closed
5 tasks done
Labels
Duplicate An existing issue was already created

Comments

@TrevTheDev
Copy link

Suggestion

Provide a way to tell the Typescript Compiler that an object's state has changed - enabling Typescript to model state, and state machines well.

πŸ” Search Terms

state

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

One solution could be to enhance type predicates to include syntax for instructing the compiler the new type of a variable and all variables sharing its reference after that call, become the new type.
E.g.

const fooBar : Foo|Bar = { ...
   set changeStateToFoo(value:string): fooBar is Foo { ... }
} as Bar
const otherRefToFooBar : Foo|Bar= fooBar  // type is `Bar`
fooBar.changeStateToFoo = 'hello' // after this call fooBar is now type `Foo` otherRefToFooBar  is now also type `Foo`

This is similar to manual reassignment, but would produce more readable and understandable code. A downside is that this could unintuitively mutate the type of a variable causing the code to be harder to reason about- but as it would produce better code than is currently the case, this remains a better solution what is currently available in Typescript.

πŸ“ƒ Motivating Example

The ability to manual change a variable or this type, would significant enhance Typescript's ability to model state and state machines.

Despite this being a fairly typical use case, Typescript current doesn't handle the following code well:

type Bar = {
    type: 'bar'
    changeStateToFoo: string
}
type Foo = {
    type: 'foo'
    sayHello: ()=>void
}

let fooBar = ():Bar=>{
    return {
    type: 'bar',
    set changeStateToFoo(value:string) { 
        delete (fooBar as any).changeStateToFoo;
        (fooBar as unknown as Foo).type = 'foo'; 
        (fooBar as unknown as Foo).sayHello = ()=>console.log(value)
        //fooBar should now change to type `Foo`
        }
    }
}
const fooBarA = fooBar()

fooBarA.changeStateToFoo = 'Hello, Foo' // after this setter, fooBarA should be type `Foo`
fooBarA.sayHello()

Implementing this code in typescript would likely require one of the follow sub optimal solutions:

/**
 * Workaround One: Type Union
 * Con: types become a fuzzy merge of multiple types addressing multiple concerns. Causing incorrect type checking in some cases
 * and requiring superfulous if(exists) checks in other cases. 
 */
type FooBar = {
    type: 'foo'|'bar'
    sayHello?: ()=>void
    changeStateToFoo?: string
}

let fooBarB = fooBar() as FooBar

fooBarB.changeStateToFoo = 'Hello, Foo'
if(fooBarB.sayHello) fooBarB.sayHello() // con: if required!
fooBarB.changeStateToFoo = 'Hello, Bar' // con: still valid - oops! 

or

/**
 * Workaround Two: Manual Reassignment
 * Con: requires arbitrary reassignment and requires writer of this code to know
 *      and manually assign the correct type which may be hard to know or complex.
 */
let fooBarC: Foo|Bar = fooBar()

fooBarC.changeStateToFoo = 'Hello, Foo'
fooBarC = (fooBarC as unknown as Foo) // con: requires superfluous reassignment and requires writer of this code to know
                                      // and manually assign the correct type which may be hard to know or complex for built up generic types.
fooBarC.sayHello()
fooBarB.changeStateToFoo = 'Hello, Bar' // pro: correct type checking!

code

πŸ’» Use Cases

Any code that uses a state machine, or transitions objects between states would significantly benefit from this.

@MartinJohns
Copy link
Contributor

Sounds like a duplicate of #41339.

@RyanCavanaugh
Copy link
Member

This is already possible with a slightly different variation

interface FooBar {
    type: 'foo'|'bar'
    sayHello?: ()=>void
    changeStateToFoo(): asserts this is { sayHello(): void }
}

let fooBarB: FooBar = fooBar() as FooBar

fooBarB.changeStateToFoo();
fooBarB.sayHello();

I don't think the principle of modifying one property via a setter and causing spooky action at a distance on other properties in the object is common enough to warrant additional modelling.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Aug 2, 2022
@TrevTheDev
Copy link
Author

TrevTheDev commented Aug 4, 2022

@RyanCavanaugh Thank you for the third work around - it is a valuable addition to the two I proposed. A key downside is that it only allows narrowing - so it doesn't allow for example an unplacedOrder to become a placedOrder (rather than UnplacedOrder & PlacedOrder) or for a placedOrder to revert back to being an unplacedOrder. I.e. like the other workarounds it also doesn't model state well.

I don't agree with your characterisation of property setters as "causing spooky action at a distance on other properties" being not common. It's kinda the point of setters that setting them changes the object and typescript modelling that well would be valuable and make it a better language.

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants