Components
New to Ember or the Octane edition specifically? You may want to read the Ember Guides’ material on Component
s first!
Glimmer Components are defined in one of three ways: with templates only, with a template and a backing class, or with only a backing class (i.e. a yield
-only component). When using a backing class, you get a first-class experience using TypeScript! For type-checking Glimmer templates as well, see Glint.
A simple component
A very simple Glimmer component which lets you change the count of a value might look like this:
Notice that there are no type declarations here – but this is actually a well-typed component. The type of count
is number
, and if we accidentally wrote something like this.count = "hello"
the compiler would give us an error.
Adding arguments and giving them a type
So far so good, but of course most components aren’t quite this simple! Instead, they’re invoked by other templates and they can invoke other components themselves in their own templates.
Glimmer components can receive both arguments and attributes when they are invoked. When you are working with a component’s backing class, you have access to the arguments but not to the attributes. The arguments are passed to the constructor, and then available as this.args
on the component instance afterward.
Since the implementation of RFC 748, Glimmer and Ember components accept a Signature
type parameter as part of their definition. This parameter is expected to be an object type with (up to) three members: Args
, Element
and Blocks
.
Args
represents the arguments your component accepts. Typically this will be an object type mapping the names of your args to their expected type. For example:
If no Args
key is specified, it will be a type error to pass any arguments to your component. You can read more about Element
and Block
in the Glint Component Signatures documentation.
Let’s imagine a component which just logs the names of its arguments when it is first constructed. First, we must define the Signature and pass it into our component, then we can use the Args
member in our Signature to set the type of args
in the constructor:
If you’re used to the classic Ember Object model, there are two important differences in the constructor itself:
we use
super
instead ofthis._super
we must call
super
before we do anything else withthis
, because in a subclassthis
is set up by running the superclass's constructor first (as implied by the JavaScript spec)
Notice that we have to start by calling super
with owner
and args
. This may be a bit different from what you’re used to in Ember or other frameworks, but is normal for sub-classes in TypeScript today. If the compiler just accepted any ...arguments
, a lot of potentially very unsafe invocations would go through. So, instead of using ...arguments
, we explicitly pass the specific arguments and make sure their types match up with what the super-class expects.
This might change in the future! If TypeScript eventually adds support for “variadic kinds”, using ...arguments
could become safe.
The types for owner
here and args
line up with what the constructor
for Glimmer components expect. The owner
is specified as unknown
because this is a detail we explicitly don’t need to know about. The args
are the Args
from the Signature we defined.
The args
passed to a Glimmer Component are available on this
, so we could change our definition to return the names of the arguments from a getter:
Understanding args
args
Now, looking at that bit of code, you might be wondering how it knows what the type of this.args
is. In the constructor
version, we explicitly named the type of the args
argument. Here, it seems to just work automatically. This works because the type definition for a Glimmer component looks roughly like this:
Not sure what’s up with <Args>
at all? We highly recommend the TypeScript Deep Dive book’s chapter on generics to be quite helpful in understanding this part.
The type signature for Component, with Args extends {} = {}
, means that the component always has a property named args
—
with the type
Args
which can be anything that extends the type
{}
– an objectand defaults to being just an empty object –
= {}
This is analogous to the type of Array
: since you can have an array of string
, or an array of number
or an array of SomeFancyObject
, the type of array is Array<T>
, where T
is the type of thing in the array, which TypeScript normally figures out for you automatically at compile time:
In the case of the Component, we have the types the way we do so that you can’t accidentally define args
as a string, or undefined
, or whatever: it has to be an object. Thus, Component<Args extends {}>
. But we also want to make it so that you can just write extends Component
, so that needs to have a default value. Thus, Component<Args extends {} = {}>
.
Giving args
a type
args
a typeNow let’s put this to use. Imagine we’re constructing a user profile component which displays the user’s name and optionally an avatar and bio. The template might look something like this:
Then we could capture the types for the profile with an interface representing the arguments:
Assuming the default tsconfig.json
settings (with strictNullChecks: true
), this wouldn't type-check if we didn't check whether the bio
argument were set.
Generic subclasses
If you'd like to make your own component subclass-able, you need to make it generic as well.
Are you sure you want to provide an inheritance-based API? Oftentimes, it's easier to maintain (and involves less TypeScript hoop-jumping) to use a compositional API instead. If you're sure, here's how!
Requiring that Args extends FancyInputArgs
means that subclasses can have more than these args, but not fewer. Specifying that the Args = FancyInputArgs
means that they default to just being FancyInputArgs
, so users don't need to supply an explicit generic type parameter here unless they're adding more arguments to the class.
Last updated