loading...

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

ferdaber profile image Ferdy Budhidharma ・3 min read

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.

Posted on by:

ferdaber profile

Ferdy Budhidharma

@ferdaber

web developer with an obsession over developer experience

Discussion

markdown guide
 

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!