You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For SSR use cases, it is useful to have a concrete type representing a component's supported attributes. Tools like JSX can use this to provide full type inference and validation. Currently, this is done by using the JSX.IntrinsicElements namespace, which can be extended with custom types like so:
Annoyingly this sometimes needs to include ComponentChildren. However there is no clear "source of truth" for a component's attributes. Ideally this would be exported from the type based on usage. Yet, in a typical component it is the presence of getAttribute() which implies that an attribute is supported with some type constraints. Ideally this would be explicitly defined somehow. I think there is an opportunity here in HydroActive to design the APIs such that we can infer and export the attributes for a component. One strawman API for this would be:
exportinterfaceProps{/* ... */}exportinterfaceAttrs{'counter-id': number};exportconstMyComponent=component<Props,Attrs>('my-component',($)=>{console.log($.attr('counter-id',Number));// Type checked. `Number` aligns with `number`.// Shorthand for:// $.read(':host', Number, attr('counter-id'));console.log($.attr('counter-id',String));// Type check error. `String` does _not_ align with `number`.console.log($.attr('counter-id',(x)=>x+1));// `x` is inferred to be type `number`? Maybe it should be `string` since attributes are always strings?});
It would be doubly cool if we could export the attribute types to a well-known interface like HTMLElementTagNameMap. That way JSX and other rendering tools could validate attributes like so:
declare global {interfaceHTMLElementTagNameAttrMap{'my-component': Attrs;}}
Then JSX could automatically set JSX.IntrinsicAttributes from that well-known type and the following would type check:
console.log(<MyComponentcounterId={1234}/>);console.log(<MyComponentcounterId={'test'}/>);// Type error.
Ultimately this isn't really "inferring" the attribute type. Fundamentally in TypeScript we can only really infer types from values, meaning that if you $.read(':host', Number, attr('counter-id')), we can infer that the return type is number from the input Number constructor. But we can't infer values from types (converting number to Number), as that is "type-based emit" which a lot of tools struggle to support for various reasons.
A consequence of this is that we can't really "infer" complex types like string | number without a complex DSL based on JS primitives. We could use something like Zod as this DSL, but I don't think I want to force that dependency. Instead, I'm thinking we can "encourage" explicit attribute types by forcing users to define the interface and then type checking the primitive. For example, we can validate that Number is compatible with number | string during type check. And by pushing the user towards writing this explicit attribute type, we could make that available through a global or other means and then connect it with tools like JSX. Another thing to think about is that $.read() of a number | string value is kind of annoying to use right now, given that it parses and validates the input. You'd basically need a try-catch to actually support such a contract.
Unfortunately, I don't think we can declaration merge JSX.IntrinsicAttributes with a mapped type from HTMLElementTagNameAttrMap, so I suspect this is something JSX will need to support directly. They don't even seem to support HTMLElementTagNameMap today, so I'm not sure how much faith I have in that. Lit SSR might be a better opportunity for integration. I'm also not 100% on how JSX handles rich types in this situation anyways and what would it really mean to have a class instance as an attribute? Serialize to JSON? Defer the serialization format, but always inputs/outputs the instance? I'm not sure.
We'd likely need a community protocol for HTMLElementTagNameAttrMap and/or support in TypeScript for the global. If so, then I think more direct support for an inferred $.attr type would go a long way towards making attribute types explicit and working more closely with these tools.
One possible contradiction here is that element attributes are kind of always strings? In a HydroActive context, we support parsing them into more complex values, but they are always strings (or I guess booleans based on this.hasAttribute()). We could say a component has an attribute type of a complex class, such as interface Attrs { user: User } but somehow we need to convert the real string value into a User. That's not done automatically, so $.attr('user', User) makes some sense (if we can define the semantics of how User gets invoked), but $.attr('user', (user: User) => user.name) doesn't. I think my takeaway from this is that the attribute type is actually the output of $.attr() converters, not the input. Maybe that's ok since any subsequent conversion should come after the initial User construction? IDK.
Regardless, there's probably still value in answering the question "Which attributes are supported by this component?", even if all of them are always strings.
The text was updated successfully, but these errors were encountered:
For SSR use cases, it is useful to have a concrete type representing a component's supported attributes. Tools like JSX can use this to provide full type inference and validation. Currently, this is done by using the
JSX.IntrinsicElements
namespace, which can be extended with custom types like so:Annoyingly this sometimes needs to include
ComponentChildren
. However there is no clear "source of truth" for a component's attributes. Ideally this would be exported from the type based on usage. Yet, in a typical component it is the presence ofgetAttribute()
which implies that an attribute is supported with some type constraints. Ideally this would be explicitly defined somehow. I think there is an opportunity here in HydroActive to design the APIs such that we can infer and export the attributes for a component. One strawman API for this would be:It would be doubly cool if we could export the attribute types to a well-known interface like
HTMLElementTagNameMap
. That way JSX and other rendering tools could validate attributes like so:Then JSX could automatically set
JSX.IntrinsicAttributes
from that well-known type and the following would type check:Ultimately this isn't really "inferring" the attribute type. Fundamentally in TypeScript we can only really infer types from values, meaning that if you
$.read(':host', Number, attr('counter-id'))
, we can infer that the return type isnumber
from the inputNumber
constructor. But we can't infer values from types (convertingnumber
toNumber
), as that is "type-based emit" which a lot of tools struggle to support for various reasons.A consequence of this is that we can't really "infer" complex types like
string | number
without a complex DSL based on JS primitives. We could use something like Zod as this DSL, but I don't think I want to force that dependency. Instead, I'm thinking we can "encourage" explicit attribute types by forcing users to define the interface and then type checking the primitive. For example, we can validate thatNumber
is compatible withnumber | string
during type check. And by pushing the user towards writing this explicit attribute type, we could make that available through a global or other means and then connect it with tools like JSX. Another thing to think about is that$.read()
of anumber | string
value is kind of annoying to use right now, given that it parses and validates the input. You'd basically need a try-catch to actually support such a contract.Unfortunately, I don't think we can declaration merge
JSX.IntrinsicAttributes
with a mapped type fromHTMLElementTagNameAttrMap
, so I suspect this is something JSX will need to support directly. They don't even seem to supportHTMLElementTagNameMap
today, so I'm not sure how much faith I have in that. Lit SSR might be a better opportunity for integration. I'm also not 100% on how JSX handles rich types in this situation anyways and what would it really mean to have a class instance as an attribute? Serialize to JSON? Defer the serialization format, but always inputs/outputs the instance? I'm not sure.We'd likely need a community protocol for
HTMLElementTagNameAttrMap
and/or support in TypeScript for the global. If so, then I think more direct support for an inferred$.attr
type would go a long way towards making attribute types explicit and working more closely with these tools.One possible contradiction here is that element attributes are kind of always strings? In a HydroActive context, we support parsing them into more complex values, but they are always strings (or I guess booleans based on
this.hasAttribute()
). We could say a component has an attribute type of a complex class, such asinterface Attrs { user: User }
but somehow we need to convert the realstring
value into aUser
. That's not done automatically, so$.attr('user', User)
makes some sense (if we can define the semantics of howUser
gets invoked), but$.attr('user', (user: User) => user.name)
doesn't. I think my takeaway from this is that the attribute type is actually the output of$.attr()
converters, not the input. Maybe that's ok since any subsequent conversion should come after the initialUser
construction? IDK.Regardless, there's probably still value in answering the question "Which attributes are supported by this component?", even if all of them are always strings.
The text was updated successfully, but these errors were encountered: