Proposal Details

What's wrong with the current Value.String behaviour?

Nothing's really wrong it's just that it doesn't quite fit the expected pattern at first glance. Value.Float, Value.Int, etc. all return or panic. Value.String coerces. This can be a problem in cases like this:

form := js.Global().Get("myform") // get <form id=myform> HTMLFormElement
formdata := js.Global().Get("FormData").New(form) // js FormData class
value := formdata.Get("chosen_os")
if slices.Contains([]string{"windows", "darwin", "linux"}, value.String()) {
  fmt.Println("Ok! Good choice!") // "why won't this work" I asked...
} else {
  fmt.Println("Bad choice.")
}

The problem? formdata.chosen_os doesn't exist. You need to call the formdata.get("chosen_os") JS method to actually get the formdata value. This is confusing because there's also a Go method called "Get". This was then compounded by the fact that this undefined value was silently coerced to a string by Value.String() without panicking like I would expect. Obviously <undefined> (which is what it stringified to) is not in the {"windows", "darwin", "linux} list and so it always returned false.

Now could this issue have even solved had I just printed fmt.Println(value.String()) to the console to see what it was? yes. But it could also be solved if the method helped me out and enforced some runtime type safety by panicking just like .Int() would have lol

I know it still needs to work when printing

I get that. I want this to ALWAYS work:

jsValue := js.ValueOf(100)
fmt.Println(jsValue)

jsValue = js.ValueOf(map[string]any{
  "hello": []any{100, "a string", true},
})
fmt.Printf("value: %v\n", jsValue)

What I propose: use the fmt.Formatter interface to hijack formatting before fmt.Stringer is even considered. Let Value.String() panic and leave formatting to Value.Format(). (it would also be good to define a Value.GoString() that you can easily call directly while we're at it)

func (v Value) Format(f fmt.State, verb rune) {
  // some logic here
}

func (v Value) GoString() string {
  // current Value.String logic here
}

This would have the added benefit of letting %d %f and other specifiers be recognizable and possibly automatically call .Int() or .Float() and then formatting them appropriately.

Something like this maybe? You might have to do some shenanigans to get around circular importing the "fmt" package idk

func (v Value) Format(f fmt.State, verb rune) {
  if verb == 'v' || verb == 's' { // ⭐ handle %v and %s just like right now with .String()!
    fmt.Fprintf(f, fmt.FormatString(f, verb), v.GoString())
  } else if verb == 't' {
    // fmt.Fprintf(f, "%t", v) where v isn't a Go bool would panic anyway.
    // v.Bool() is OK to panic itself instead.
    fmt.Fprintf(f, fmt.FormatString(f, verb), v.Bool())
  } else if verb == 'b' || verb == 'c' || verb == 'd' || verb == 'o' ... {
    fmt.Fprintf(f, fmt.FormatString(f, verb), v.Int()) // OK to panic
  } else if verb == 'b' || verb == 'e' || verb == 'f' ... {
    fmt.Fprintf(f, fmt.FormatString(f, verb), v.Float()) // OK to panic
  } else if ... // etc for as many "act like a $TYPE" cases as you want
  } else {
    // fall back to default by wrapping in a newtype without a .Format()
    type valueAlias = Value
    type Value valueAlias // same name so that %T shows "js.Value"
    fmt.Fprintf(f, fmt.FormatString(f, verb), Value(v))
  }
}

anyways the core idea: use .Format() to let everything behaviour-wise stay the same with regards to fmt.Print() and friends so that .String() can really mean "get this as a string or else panic if it's not really a string".

DOWNSIDES TO THIS CHANGE: It's a change. Any change in behaviour can be bad. In this case though, syscall/js is experimental and this shouldn't change behaviour that much. The one case where things have to change is something like this:

// print a generic js value to the console
fmt.Printf("%s\n", jsValue.String())

...which you can't really do much about. semantically, at first glance I would think that means "this jsValue is for sure a string type. get it as a go string" but that's NOT what it means. it means "get this jsValue of any type as a human-readable string repr". I think that belongs in .GoString() or .Format() but that's a change from how it is now. And that means that this could beak such code if it's written like above.

Basically, I want some type-checking method to get a js.Value as a string or else panic similar to .Int() and .Float() and .Bool(). I would like to revive https://github.com/golang/go/issues/29642 discussion to find a solution to this so that someone else doesn't spend multiple hours debugging something like I was.

Alternatives

https://github.com/golang/go/issues/29642 had some discussion about this. .Format() was never mentioned though.

  • Add a MustString()
  • Add a AsString()
  • Do nothing

Comment From: ianlancetaylor

CC @golang/js

Comment From: Zxilly

Maybe we can add more detailed documentation? I'm not too keen on changing the existing API.

Comment From: gopherbot

Change https://go.dev/cl/654515 mentions this issue: syscall/js: add Value.Format and make Value.String stricter

Comment From: aditya270520

@gopherbot done

Comment From: johanbrandhorst

Thanks for the prototype @aditya270520, this change can serve as a place to test this proposal before it is accepted, but it will not be merged until this proposal has been accepted.

Comment From: aditya270520

what I should do to get this accepted @johanbrandhorst

Comment From: johanbrandhorst

It will need to be picked up by the proposal review committee and moved through the proposal stages. Usually the committee will wait for a consensus in the discussion before making a decision. I think since this is a change in behavior it is pretty unlikely to be accepted, even though the Wasm port is experimental.