DEV Community

Ferdy Budhidharma
Ferdy Budhidharma

Posted on

16 5

TypeScript and JSX Part IV - Typing the props of a component

In the last post we learned how TypeScript type checks JSX children with respect to a constructor's props. This time we'll delve deeper into the rest of a component's props and how those are used for typechecking which are valid when creating JSX.

TypeScript treats intrinsic, function, and class components differently when figuring out which attributes can be assigned to a JSX expression constructed by these components.

  • for intrinsic element constructors (lower-case tag name), it looks at the type of the same property name in JSX.IntrinsicElements
  • for function element constructors, it looks at the type of the first parameter in the call signature
  • for class-based element constructors, it looks at the type of the instance property that has the same name under JSX.ElementAttributesProperty, and if that doesn't exist, it will look at the type of the first parameter in the constructor call signature

Let's look at each case in detail:

Intrinsic Element Constructors

If your JSX namespace looks like this:

interface HTMLAttributes<T> {
  children?: ReactNode
  className?: string
  id?: string
  onClick?(event: MouseEvent<T>): void
  ref?: { current?: T }
}

namespace JSX {
  interface IntrinsicElements {
    a: HTMLAttributes<HTMLAnchorElement>
    button: HTMLAttributes<HTMLButtonElement>
    div: HTMLAttributes<HTMLElement>
    span: HTMLAttributes<HTMLElement>
  }
}

Then for an anchor element, the available attributes you can give an <a /> tag equivalent to JSX.IntrinsicElements['a']:

interface AnchorProps {
  children?: ReactNode
  className?: string
  id?: string
  onClick?(event: MouseEvent<HTMLAnchorElement>): void
  ref?: { current?: HTMLAnchorElement }
}

declare const props: AnchorProps

const myAnchor = <a {...props} />

Function Element Constructors

If your component looks like this:

interface Props {
  onClick?(event: MouseEvent<HTMLButtonElement>): void
  disabled?: boolean
  label: string
}

function MyButton(
  props: Props & { children?: ReactNode },
  some?: any,
  other?: any,
  parameters?: any
) {
  return <button />
}

Then the available attributes are Props along with { children?: ReactNode }, because that's the type of the first parameter in the function. Note that TypeScript will respect optional and required properties in the type of the props as well:

const button = <MyButton /> // error because label is marked as required in Props!

Class Element Constructors

If your class looks like this, and you have a JSX namespace like this:

interface Props {
  onClick?(event: MouseEvent<HTMLButtonElement>): void
  disabled?: boolean
  label: string
}

class MyComponent {
  _props: Props

  constructor(props: Props & { children?: ReactNode }) {
    this.props = props
  }

  render() {
    return <button />
  }
}

namespace JSX {
  interface ElementClass {
    render(): any
  }

  interface ElementAttributesProperty {
    _props: {}
  }
}

Then the available attributes for MyComponent are Props (note that this one cannot have children), because the instance type of MyComponent has a property called _props, which is the same as the property name inside JSX.ElementAttributesProperty. If that interface in the JSX namespace wasn't there, it would instead look at the first parameter's type in the constructor, which is Props with { children?: ReactNode }.

This covers all of the "internal" props that a component can use within it. In React, however, we have a concept of "external" props which is the actual contract of what you can pass into a JSX expression constructed by the component. An example of how external props differ from internal props would be ref and key, as well as defaultProps:

  • ref and key are not available to be used inside a component's implementation, but key can always be assigned to any JSX expression in React, and refs can be assigned to any class-based and intrinsic JSX expressions, as well as function based expressions using forwardRef.
  • defaultProps allows a specific prop to always be defined inside a component's implementation, but optional when assigning that same prop in a JSX expression of that component.

In the next post we will learn how TypeScript allows this to happen using some more JSX namespace magic.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
denniscual profile image
Dennis Cual

Great posts and I learned a lot. When are you gonna publish the next post? Hoping that you can publish it and patiently waiting!

Kudos!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay