This article was first published on the wechat public account “Programmer Interviewer”.

TypeScript and React in three thousand words

Most of the time we know the basics of TypeScript, but it’s not enough to use it in real projects. For example, most front-end developer projects rely on frameworks, so we need to talk about how React and TypeScript can be used together.

Getting started with TypeScript frameworks can be quite frustrating if you only know the basics of TypeScript (as I did myself), but if you’re a React developer, be sure to read this article before trying it out.

Quickly launch TypeScript version of React

Writing React code with TypeScript requires at least two additional libraries in addition to the TypeScript library:

yarn add -D @types/{react,react-dom}
Copy the code

For those of you wondering, what is this library that starts with @types?

Because many JavaScript libraries do not provide their own TypeScript declaration files, TypeScript users cannot enjoy the types provided by the libraries. One project DefinitelyTyped emerged in the community. It defines the declarations of most JavaScript libraries on the market today, and people can enjoy the library-related type definitions when they download the @types declarations of JavaScript libraries.

Of course, we chose to use the react startup template provided by TypeScript for convenience.

create-react-app react-ts-app --scripts-version=react-scripts-ts
Copy the code

Stateless component

Now that we’ve initialized the above template, it’s time to start writing the code.

A stateless component is a very common React component that is used to display the UI. The original react template has a logo image, which can be encapsulated as a logo component.

In JavaScript we tend to encapsulate components like this:

import * as React from 'react'

export const Logo = props= > {
    const { logo, className, alt } = props

    return (
        <img src={logo} className={className} alt={alt} />)}Copy the code

In TypeScript, however, an error is reported:

We didn’t define the type of props. Let’s use interface to define the type of props.

import * as React from 'react'interface IProps { logo? : string className? : string alt? : string }export const Logo = (props: IProps) = > {
    const { logo, className, alt } = props

    return (
        <img src={logo} className={className} alt={alt} />)}Copy the code

This may seem fine in this case, but do we have to go to the children type when we want to use children?

Like this:

interface IProps { logo? : string className? : string alt? : string children? : ReactNode }Copy the code

There is a more formal and simpler approach,type SFC

, which already defines the children type.

We just need to use it like this:

export const Logo: React.SFC<IProps> = props => {
    const { logo, className, alt } = props

    return (
        <img src={logo} className={className} alt={alt} />
    )
}
Copy the code

We can now replace the logo component in app. TSX, and you can see the code prompt for the props:

If our component is a common component in the business, we can even add comments:

Interface IProps {/** * logo */ logo? : string className? : string alt? : string }Copy the code

This way, when other colleagues call this component, there will even be a comment in addition to the code prompt:

Stateful component

Now suppose we start writing a Todo application:

First we need to write a todoInput component:

If we were to use JavaScript, we would get a bunch of errors as soon as we started

Stateful components require props as well as props. For class components, we need generic support (Component

). Therefore, we need to pass in types of props and state so that we can use props and state normally.

,>

import * as React from 'react'

interface Props {
    handleSubmit: (value: string) => void
}

interface State {
    itemText: string
}

export class TodoInput extends React.Component<Props, State> {
    constructor(props: Props) {
        super(props)
        this.state = {
            itemText: ' '}}}Copy the code

We need to add Readonly to Props and State because our data is immutable.

This is not necessary because the React declaration file automatically wraps the above type for us and marks it as readonly.

As follows:

Next we need to add the component method. In most cases, this method is private to the component. In this case we need to add the access control character private.

    private updateValue(value: string) {
        this.setState({ itemText: value })
    }
Copy the code

Now, this is also a tricky type that you often encounter, if we want to get the ref of a component, what do we do?

For example, we need to make the Input component focus after the component is updated.

First, we need to create a ref with React. CreateRef and import it in the corresponding component.

private inputRef = React.createRef<HTMLInputElement>()
...

<input
    ref={this.inputRef}
    className="edit"
    value={this.state.itemText}
/>
Copy the code

Note that in createRef you need a generic type, and that generic type is the type of the ref component, because this is the input component, so the type is HTMLInputElement, and of course if it’s a div component it’s HTMLDivElement.

The controlled components

Moving on to the TodoInput component, which is also a controlled component, we call this.setState to update the state whenever we change the value of the input, and we use the “event” type.

React events are actually synthetic events, which means they are processed by React, so they are not native events. Therefore, we usually need to define the event types in React.

For events in the input component onChange, we typically declare them like this:

private updateValue(e: React.ChangeEvent<HTMLInputElement>) {
    this.setState({ itemText: e.target.value })
}
Copy the code

When we need to submit a form, we need to define the event type like this:

    private handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault()
        if(! this.state.itemText.trim()) {return
        }

        this.props.handleSubmit(this.state.itemText)
        this.setState({itemText: ' '})}Copy the code

So how do we remember so many types of definitions? Do I have to go through all sorts of searches to define a type for other events I haven’t seen before? The trick here is that when we enter the name of the event in the component, we are prompted to define the event. We just use the type in the prompt.

The default attribute

React sometimes uses a lot of default properties, especially when we’re writing generic components. One of the tricks to use default properties in React is to use class to declare both types and create initial values.

Going back to our project, let’s say we need to pass properties to the input component via props and need initial values. We can simplify the code by using class.

// props.type.ts interface InputSetting { placeholder? : string maxlength? : number }exportclass TodoInputProps { public handleSubmit: (value: string) => void public inputSetting? : InputSetting = { maxlength: 20, placeholder:'Please enter todo',}}Copy the code

Back in the TodoInput component, we pass the component directly with class as type and instantiate the class as the default property.

Using class as the props type and producing default attribute instances has the following benefits:

  • Small amount of code: written once and used either as a type or instantiated as a value
  • Avoid errors: Write separately once one side causes handwriting errors to be undetectable

This approach is fine, but later we will find that even though we have declared the default property, inputSetting may not be defined when we use it.

One of the fastest solutions in this case is to add! , which tells the compiler that this is not undefined, thus avoiding errors.

If you think this method is too rude, you can make a simple judgment by choosing the ternary operator:

If you still feel this way is a little complicated, because if this is too much, we need additional written quite a lot of conditions, and more importantly, we clearly have declared the value, you should not do the condition judgment, there should be a way to let the compiler deduces this type is undefined, it involves some senior types.

Use advanced types to resolve default attribute errors

We now need to declare the value of defaultProps:

const todoInputDefaultProps = {
    inputSetting: {
        maxlength: 20,
        placeholder: 'Please enter todo',}}Copy the code

Next, define the props type of the component

type Props = {
    handleSubmit: (value: string) => void
    children: React.ReactNode
} & Partial<typeof todoInputDefaultProps>
Copy the code

Partial is used to make all attributes of a type optional, as in the following case:

{ inputSetting? : { maxlength: number; placeholder: string; } | undefined; }Copy the code

So now we have no problem using Props?

export class TodoInput extends React.Component<Props, State> {

    public static defaultProps = todoInputDefaultProps

...

    public render() {
        const { itemText } = this.state
        const { updateValue, handleSubmit } = this
        const { inputSetting } = this.props

        return (
            <form onSubmit={handleSubmit} >
                <input maxLength={inputSetting.maxlength} type='text' value={itemText} onChange={updateValue} />
                <button type='submit'</button> </form>)}... }Copy the code

We still get an error when we see:

In this case, we need a function to convert the properties of defaultProps with declared values from “optional” to “non-optional”.

Let’s start with a function like this:

const createPropsGetter = <DP extends object>(defaultProps: DP) => {
    return <P extends Partial<DP>>(props: P) => {
        type PropsExcludingDefaults = Omit<P, keyof DP>
        type RecomposedProps = DP & PropsExcludingDefaults

        return (props as any) as RecomposedProps
    }
}
Copy the code

This function takes a defaultProps object,

which is a generic constraint, which means that DP is an object, and returns an anonymous function.

Look at this anonymous function, this function has a generic P, this generic P also been constraint, namely < P extends Partial < DP > >, which means that the generic must contain an optional DP type (actually this generic P is the component of the incoming Props type).

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

type Omit<P, keyof DP> = Pick<P, Exclude<keyof P, keyof DP>>
Copy the code

The type alias RecomposedProps combines the DP type of the default attribute with the Props type, which has the default attribute removed.

This function does only one thing. After removing the optional type of defaultProps, it adds the required type of defaultProps to form a new Props type. The properties of the defaultProps type become required.

This function may be difficult for beginners to understand, but it is a comprehensive application that involves the advanced types in TypeScript documents.

The complete code is as follows:

import * as React from 'react'

interface State {
    itemText: string
}

type Props = {
    handleSubmit: (value: string) => void
    children: React.ReactNode
} & Partial<typeof todoInputDefaultProps>

const todoInputDefaultProps = {
    inputSetting: {
        maxlength: 20,
        placeholder: 'Please enter todo',}}export const createPropsGetter = <DP extends object>(defaultProps: DP) => {
    return <P extends Partial<DP>>(props: P) => {
        type PropsExcludingDefaults = Omit<P, keyof DP>
        type RecomposedProps = DP & PropsExcludingDefaults

        return (props as any) as RecomposedProps
    }
}

const getProps = createPropsGetter(todoInputDefaultProps)

export class TodoInput extends React.Component<Props, State> {

    public static defaultProps = todoInputDefaultProps

    constructor(props: Props) {
        super(props)
        this.state = {
            itemText: ' '
        }
    }

    public render() {
        const { itemText } = this.state
        const { updateValue, handleSubmit } = this
        const { inputSetting } = getProps(this.props)

        return (
            <form onSubmit={handleSubmit} >
                <input maxLength={inputSetting.maxlength} type='text' value={itemText} onChange={updateValue} />
                <button type='submit'> todo</button> </form>)} private updateValue(e: react. ChangeEvent<HTMLInputElement>) {this.setState({itemText: e.target.value }) } private handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault()if(! this.state.itemText.trim()) {return
        }

        this.props.handleSubmit(this.state.itemText)
        this.setState({itemText: ' '}}})Copy the code

High order component

How to use HOC in TypeScript has always been a challenge, but here’s a more general approach.

Moving on to the TodoInput component, where we’ve been customizing the properties of the input with inputSetting, we now need to wrap the TodoInput with a HOC that uses a higher-order component to inject props into the TodoInput.

Our higher-order function is as follows:

import * as hoistNonReactStatics from 'hoist-non-react-statics'
import * as React from 'react'

type InjectedProps = Partial<typeof hocProps>

const hocProps = {
    inputSetting: {
        maxlength: 30,
        placeholder: 'Please enter to-do list',}}export const withTodoInput = <P extends InjectedProps>(
  UnwrappedComponent: React.ComponentType<P>,
) => {
  type Props = Omit<P, keyof InjectedProps>

  class WithToggleable extends React.Component<Props> {

    public static readonly UnwrappedComponent = UnwrappedComponent

    public render() {

      return (
        <UnwrappedComponent
        inputSetting={hocProps}
        {...this.props as P}
        />
      );
    }
  }

  return hoistNonReactStatics(WithToggleable, UnwrappedComponent)
}
Copy the code

If you understand the previous section, there should be no difficulty here.

Here we P said of components to the HOC props, React.Com ponentType < P > is the React FunctionComponent < P > | React. ClassComponent < P > the alias, Indicates that the component passed to HOC can be a class component or a function component.

The rest of the Omit Omit as P and so on are all discussed, readers can understand by themselves. We don’t want to explain it line by line like the previous section.

Just use it like this:

const HOC = withTodoInput<Props>(TodoInput)
Copy the code

summary

We’ve summarized some of the most common components written in TypeScript. With this article, you can solve most of the problems with TypeScript in React.