Repository: a-h/templ (Go)

Issue:
proposal: Allow for primitives & fmt.Stringer in addition to string in templates
Discussion happened previous at #609 but focused on integers.  I understand @a-h position on clarity around integers and currency.  It seems like in the currency case having a `type Currency int64` with a `String() string` function would allow it to be a valid `fmt.Stringer` and avoid ambiguity.  Personally i'd like to see all primitive types and `fmt.Stringer` accepted by the template.  Since the code is generated you aren't paying the reflection cost each time.  `strconv` already has functions for Bool, Float, Int, etc.  If you want to have more control, implement the `Stringer` interface


Discussion: Looking at our proposal template I think we can answer some questions straight away:


Backwards compatibility - This change should be completely backwards compatible, the generated code may be slightly different but shouldn't be breaking.
Threat model - I imagine in this change we would convert to strings and do the same escaping we already do.
Go version - We may introduce generics here in runtime, but that is fine, the oldest supported version has generics.
Automatic migration - Not applicable?
Compile time vs runtime errors - I believe that we can use generics here which would hopefully provide useful errors.
Documentation - We can add to https://templ.guide/syntax-and-usage/expressions
Examples - A few examples around primitives and how to implement stringer would be useful (currency is a great example)

There is also the question:

Correctness over time - How can we reduce the risk of defects both now, and in future releases?

But overall, I think that this proposal stands up to our requirements, it is just a case of whether moving from opaque string rendering to transparent (in some cases) is what we want. 

I think we can solve @a-h concern of displaying `nil` to a user for example my not supporting any types that are nillable.

The reason the last one was closed is because we didn't get much engagement or discussion in the issue to form a consensus or even a majority.

Per a short discussion on Discord with @joerdav .  It might make sense to use a `TemplString() string` interface as there can be a difference between how you would logging something and want it displayed in a Templ template.  I think the display/nil concern is mitigated as you can either error out in the generated code via an if check, or possibly check and fallback to a `TemplNilString()` interface if satisfied.
![image](https://github.com/user-attachments/assets/10643c3e-93c8-45a7-9c5c-e50802e7ab9d)

I think that this is a sensible change for templ. At the very least I think allowing all primitives should be implemented. And maybe the `type Stringer interface { TemplString() string }` interface too?

I think this can be implemented if @a-h is in agreement?



Fix (patch):
diff --git a/docs/docs/03-syntax-and-usage/04-expressions.md b/docs/docs/03-syntax-and-usage/04-expressions.md
index 22b840e83..3019bec5a 100644
--- a/docs/docs/03-syntax-and-usage/04-expressions.md
+++ b/docs/docs/03-syntax-and-usage/04-expressions.md
@@ -1,14 +1,24 @@
 # Expressions
 
-## String expressions
+## Interpolation expressions
 
-Within a templ element, expressions can be used to render strings. Content is automatically escaped using context-aware HTML encoding rules to protect against XSS and CSS injection attacks.
+Within a templ element, expressions can be used to interpolate go values. Content is automatically escaped using context-aware HTML encoding rules to protect against XSS and CSS injection attacks.
 
-String literals, variables and functions that return a string can be used. 
+Literals, variables and functions that return a value can be used. 
+
+The supported types for interpolation are:
+
+- Strings
+- Numbers (`int`, `uint`, `float32`, `complex64` etc.)
+- Booleans
+
+:::note
+Any type based on the above list can be used, for example `type Age int` or `type Name string`.
+:::
 
 ### Literals
 
-You can use Go string literals.
+You can use Go literals.
 
 ```templ title="component.templ"
 package main
@@ -16,26 +26,28 @@ package main
 templ component() {
   <div>{ "print this" }</div>
   <div>{ `and this` }</div>
+  <div>Number of the day: { 1 }</div>
 }
 ```
 
 ```html title="Output"
-<div>print this</div><div>and this</div>
+<div>print this</div><div>and this</div><div>Number of the day: 1</div>
 ```
 
 ### Variables
 
-Any Go string variable can be used, for example:
+Any supported Go variable can be used, for example:
 
-* A string function parameter.
+* A function parameter.
 * A field on a struct.
-* A variable or constant string that is in scope.
+* A variable or constant that is in scope.
 
 ```templ title="/main.templ"
 package main
 
 templ greet(prefix string, p Person) {
   <div>{ prefix } { p.Name }{ exclamation }</div>
+  <div>Congratulations on being { p.Age }!</div>
 }
 ```
 
@@ -44,24 +56,25 @@ package main
 
 type Person struct {
   Name string
+  Age  int
 }
 
 const exclamation = "!"
 
 func main() {
-  p := Person{ Name: "John" }
+  p := Person{ Name: "John", Age: 42 }
   component := greet("Hello", p) 
   component.Render(context.Background(), os.Stdout)
 }
 ```
 
 ```html title="Output"
-<div>Hello John!</div>
+<div>Hello John!</div><div>Congratulations on being 42!</div>
 ```
 
 ### Functions
 
-Functions that return `string` or `(string, error)` can be used.
+Functions that return a value, or a value-error tuple can be used.
 
 ```templ title="component.templ"
 package main
diff --git a/flake.nix b/flake.nix
index d6f2fae73..716221215 100644
--- a/flake.nix
+++ b/flake.nix
@@ -41,7 +41,7 @@
             name = "templ";
             subPackages = [ "cmd/templ" ];
             src = gitignore.lib.gitignoreSource ./.;
-            vendorHash = "sha256-JVOsjBn1LV8p6HHelfAO1Qcqi/tPg1S3xBffo+0aplE=";
+            vendorHash = "sha256-x0CLfCjgfgFp7vAVXwrLDLm+SpgHv3v3dW+Vf3WlTJU=";
             CGO_ENABLED = 0;
             flags = [
               "-trimpath"
diff --git a/go.mod b/go.mod
index ae0b0462b..392444d61 100644
--- a/go.mod
+++ b/go.mod
@@ -15,16 +15,16 @@ require (
 	github.com/google/go-cmp v0.6.0
 	github.com/natefinch/atomic v1.0.1
 	github.com/rs/cors v1.11.0
-	golang.org/x/mod v0.20.0
-	golang.org/x/net v0.37.0
-	golang.org/x/sync v0.10.0
-	golang.org/x/tools v0.24.0
+	golang.org/x/mod v0.24.0
+	golang.org/x/net v0.39.0
+	golang.org/x/sync v0.13.0
+	golang.org/x/tools v0.32.0
 )
 
 require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
 )
 
 // replace github.com/a-h/parse => /Users/adrian/github.com/a-h/parse
diff --git a/go.sum b/go.sum
index 564d2037e..608f274d9 100644
--- a/go.sum
+++ b/go.sum
@@ -23,15 +23,15 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0
 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
 github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
 github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
diff --git a/runtime.go b/runtime.go
index b55459b63..94a743393 100644
--- a/runtime.go
+++ b/runtime.go
@@ -578,9 +578,29 @@ func ReleaseBuffer(b *bytes.Buffer) {
 	bufferPool.Put(b)
 }
 
+type ints interface {
+	~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+type uints interface {
+	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
+}
+
+type floats interface {
+	~float32 | ~float64
+}
+
+type complexNumbers interface {
+	~complex64 | ~complex128
+}
+
+type stringable interface {
+	ints | uints | floats | complexNumbers | ~string | ~bool
+}
+
 // JoinStringErrs joins an optional list of errors.
-func JoinStringErrs(s string, errs ...error) (string, error) {
-	return s, errors.Join(errs...)
+func JoinStringErrs[T stringable](s T, errs ...error) (string, error) {
+	return fmt.Sprint(s), errors.Join(errs...)
 }
 
 // Error returned during template rendering.