本文内容摘自:三千字讲清TypeScript与React的实战技巧

vscode报错:[ts] 对象可能“未定义”的解决方法

  • 加**!**,它的作用就是告诉编译器这里不是undefined,从而避免报错
  • 使用三目运算符做一个简单的判断
  • 使用TypeScript的高级类型,编译器自己推导出这里的类型不是undefined

默认属性

React中有时候会运用很多默认属性,尤其是在我们编写通用组件的时候,之前我们介绍过一个关于默认属性的小技巧,就是利用class来同时声明类型和创建初始值。
再回到我们这个项目中,假设我们需要通过props来给input组件传递属性,而且需要初始值,我们这个时候完全可以通过class来进行代码简化。

  1. // props.type.ts
  2. interface InputSetting {
  3. placeholder?: string
  4. maxlength?: number
  5. }
  6. export class TodoInputProps {
  7. public handleSubmit: (value: string) => void
  8. public inputSetting?: InputSetting = {
  9. maxlength: 20,
  10. placeholder: '请输入todo',
  11. }
  12. }

再回到TodoInput组件中,我们直接用class作为类型传入组件,同时实例化类,作为默认属性。

TypeScript高级类型解决React项目中默认属性报错 - 图1

用class作为props类型以及生产默认属性实例有以下好处:

  • 代码量少:一次编写,既可以作为类型也可以实例化作为值使用
  • 避免错误:分开编写一旦有一方造成书写错误不易察觉

这种方法虽然不错,但是之后我们会发现问题了,虽然我们已经声明了默认属性,但是在使用的时候,依然显示inputSetting可能未定义。

TypeScript高级类型解决React项目中默认属性报错 - 图2

在这种情况下有一种最快速的解决办法,就是加**!**,它的作用就是告诉编译器这里不是undefined,从而避免报错。

TypeScript高级类型解决React项目中默认属性报错 - 图3

如果你觉得这个方法过于粗暴,那么可以选择三目运算符做一个简单的判断:

TypeScript高级类型解决React项目中默认属性报错 - 图4

如果你还觉得这个方法有点繁琐,因为如果这种情况过多,我们需要额外写非常多的条件判断,而更重要的是,我们明明已经声明了值,就不应该再做条件判断了,应该有一种方法让编译器自己推导出这里的类型不是undefined,这就涉及到一些高级类型了。

利用高级类型解决默认属性报错

我们现在需要先声明defaultProps的值:

const todoInputDefaultProps = {
    inputSetting: {
        maxlength: 20,
        placeholder: '请输入todo',
    }
}

接着定义组件的props类型

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

Partial的作用就是将类型的属性全部变成可选的,也就是下面这种情况:

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

那么现在我们使用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' >添加todo</button>
            </form>
        )
    }
...
}

我们看到依旧会报错:

TypeScript高级类型解决React项目中默认属性报错 - 图5

其实这个时候我们需要一个函数,将defaultProps中已经声明值的属性从『可选类型』转化为『非可选类型』。
我们先看这么一个函数:

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
    }
}

这个函数接受一个defaultProps对象,<DP extends object>这里是泛型约束,代表DP这个泛型是个对象,然后返回一个匿名函数。
再看这个匿名函数,此函数也有一个泛型P,这个泛型P也被约束过,即<P extends Partial<DP>>,意思就是这个泛型必须包含可选的DP类型(实际上这个泛型P就是组件传入的Props类型)。
接着我们看类型别名PropsExcludingDefaults,看这个名字你也能猜出来,它的作用其实是剔除Props类型中关于defaultProps的部分,很多人可能不清楚Omit这个高级类型的用法,其实就是一个语法糖:

type Omit<P, keyof DP> = Pick<P, Exclude<keyof P, keyof DP>>

而类型别名RecomposedProps则是将默认属性的类型DP与剔除了默认属性的Props类型结合在一起。
其实这个函数只做了一件事,把可选的defaultProps的类型剔除后,加入必选的defaultProps的类型,从而形成一个新的Props类型,这个Props类型中的defaultProps相关属性就变成了必选的。

这个函数可能对于初学者理解上有一定难度,涉及到TypeScript文档中的高级类型,这算是一次综合应用。

完整代码如下:

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: '请输入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: ''})
    }
}