使用ReactJS和TypeScript实现类型安全的select onChange事件

115

我已经想出如何在 SELECT 元素上绑定事件处理程序,但需要使用对任意事件的丑陋类型转换。

是否有可能以类型安全的方式检索值,而无需转换为任何类型?

import React = require('react');

interface ITestState {
    selectedValue: string;
}

export class Test extends React.Component<{}, ITestState> {

    constructor() {
        super();
        this.state = { selectedValue: "A" };
    }

    change(event: React.FormEvent) {
        console.log("Test.change");
        console.log(event.target); // in chrome => <select class="form-control" id="searchType" data-reactid=".0.0.0.0.3.1">...</select>

        // Use cast to any works but is not type safe
        var unsafeSearchTypeValue = ((event.target) as any).value;

        console.log(unsafeSearchTypeValue); // in chrome => B

        this.setState({
            selectedValue: unsafeSearchTypeValue
        });
    }

    render() {
        return (
            <div>
                <label htmlFor="searchType">Safe</label>
                <select className="form-control" id="searchType" onChange={ e => this.change(e) } value={ this.state.selectedValue }>
                    <option value="A">A</option>
                    <option value="B">B</option>
                </select>
                <h1>{this.state.selectedValue}</h1>
            </div>
        );
    }
}

2
类型安全是什么意思? - François Richard
1
我猜这意味着使用TypeScript编译,并检查所有变量分配是否符合其类型。当然,我们谈论的是TypeScript,而不是JavaScript。 - Cyril Gandon
9个回答

125

我尝试使用 React.FormEvent<HTMLSelectElement>,但这导致编辑器出现了错误,尽管代码中没有看到任何 EventTarget

属性“value”在类型“EventTarget”的值上不存在

然后我将 React.FormEvent 改为 React.ChangeEvent,它有所帮助:

private changeName(event: React.ChangeEvent<HTMLSelectElement>) {
    event.preventDefault();
    this.props.actions.changeName(event.target.value);
}

4
如果我访问 e.target.value,那么 FormEvent 总是会显示错误,这应该是最佳答案。 - Capaj
这个问题已经得到了修正,现在我可以成功访问 event.target.value 而没有任何问题;它被定义为 string 类型,因此根据具体情况可能需要进行双重转换。例如:event.target.value as unknown as number - TheDiveO

80

自从我把我的typings升级到React 0.14.43以后(我不确定这是何时引入的),React.FormEvent类型现在是泛型的,这样就不需要进行强制转换了。

import React = require('react');

interface ITestState {
    selectedValue: string;
}

export class Test extends React.Component<{}, ITestState> {

    constructor() {
        super();
        this.state = { selectedValue: "A" };
    }

    change(event: React.FormEvent<HTMLSelectElement>) {
        // No longer need to cast to any - hooray for react!
        var safeSearchTypeValue: string = event.currentTarget.value;

        console.log(safeSearchTypeValue); // in chrome => B

        this.setState({
            selectedValue: safeSearchTypeValue
        });
    }

    render() {
        return (
            <div>
                <label htmlFor="searchType">Safe</label>
                <select className="form-control" id="searchType" onChange={ e => this.change(e) } value={ this.state.selectedValue }>
                    <option value="A">A</option>
                    <option value="B">B</option>
                </select>
                <h1>{this.state.selectedValue}</h1>
            </div>
        );
    }
}

17
截至2016年12月,必须使用event.currentTarget.value来获取表单中的数据。 - Martin Majewski
1
戴夫,请考虑更新你的示例。 - Alex
2
@MartinMajewski,我已经编辑过了,使用了currentTarget(http://joequery.me/code/event-target-vs-event-currenttarget-30-seconds/是一个很好的解释)。 - davestevens

26

在我的情况下,onChange事件被标记为React.ChangeEvent<HTMLSelectElement>

onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
  console.warn('onChange TextInput value: ' + e.target.value);
}}

12

更新:React 的官方类型定义现在已经将事件类型作为泛型类型包含了一段时间,因此您现在可以进行完整的编译时检查,本答案已过时。


是否有可能以类型安全的方式检索该值而无需转换为 any 类型?

是的。 如果您确定处理程序附加到的元素,则可以执行以下操作:

<select onChange={ e => this.selectChangeHandler(e) }>
    ...
</select>
private selectChangeHandler(e: React.FormEvent)
{
    var target = e.target as HTMLSelectElement;
    var intval: number = target.value; // Error: 'string' not assignable to 'number'
}

实时演示

TypeScript编译器将允许此类型断言,因为HTMLSelectElementEventTarget。之后,应该是类型安全的,因为你知道e.target是一个HTMLSelectElement,因为你刚刚将事件处理程序附加到它上面了。

然而,为了保证类型安全(在重构时很重要),还需要检查实际运行时类型:

if (!(target instanceof HTMLSelectElement))
{
    throw new TypeError("Expected a HTMLSelectElement.");
}

我想这比强制转换为任何类型要好,但你仍然在使用类型断言,而这正是我试图避免的。 - davestevens
是的,但是由于您完全确定 e.target 是一个 HTMLSelectElement,因此这种类型断言是_安全_的,并且所有后续的类型检查也是固有的安全性,例如 target.value。如果您想使它绝对防弹,您还可以运行时检查其类型,如果 e.target 不是正确类型,则抛出 TypeError。这实际上永远不会发生,但它增加了一层_保证_类型安全。 - John Weisz
我对类型安全的理解是由编译器强制执行类型检查。这意味着没有类型断言,也不依赖于运行时异常。 - davestevens
1
我之前评论说事件处理程序应该是公共的,而不是私有的。但我后来意识到这是不正确的。即使在运行时我们有私有方法(希望我们将来会有 https://github.com/tc39/proposal-private-methods),一个私有方法在这里仍然可以正常工作,因为React不需要直接调用私有方法,而只需要调用你传递给 onChange 属性的箭头函数。 - Matt Browne

10

它起作用了:

type HtmlEvent = React.ChangeEvent<HTMLSelectElement>

const onChange: React.EventHandler<HtmlEvent> = 
   (event: HtmlEvent) => { 
       console.log(event.target.value) 
   }

9
最简单的方法是在接收值的变量中添加类型,如下所示:
var value: string = (event.target as any).value;

或者你可以像这样将 value 属性和 event.target 一起转换:

var value = ((event.target as any).value as string);

编辑:

最后,您可以在单独的.d.ts文件中定义EventTarget.value是什么。然而,类型必须与其他地方使用的类型兼容,否则您最终仍然会再次使用any

globals.d.ts

interface EventTarget {
    value: any;
}

4
我希望完全去掉“像任何”的类型转换。 - davestevens
谢谢您的想法,但那仍然没有回答我的问题。我认为答案是需要修改React的.d.ts文件,以便SELECT元素的onChange签名使用新的SelectFormEvent。否则,就像您指出的那样,需要进行强制转换。我想我可以将其封装在MYSELECT标记中。 - davestevens
1
好的,不要忘记回答并接受你的答案 :) 自答 - thoughtrepo
一种更好的添加EventTarget.value的方法是通过任何.ts文件增强全局eventTarget: declare global { interface EventTarget { value: any; }} - JasonS

6

JSX:

<select value={ this.state.foo } onChange={ this.handleFooChange }>
    <option value="A">A</option>
    <option value="B">B</option>
</select>

TypeScript:

private handleFooChange = (event: React.FormEvent<HTMLSelectElement>) => {
    const element = event.target as HTMLSelectElement;
    this.setState({ foo: element.value });
}

3
据我所知,目前这是不可能的 - 总是需要进行转换。
要实现这一点,需要修改React的.d.ts文件,以便SELECT元素的onChange签名使用新的SelectFormEvent。新事件类型将公开target,target公开value。然后,代码就可以是类型安全的了。
否则,总会需要将其转换为任何类型。
我可以将所有内容封装在MYSELECT标记中。

0
除了@thoughtrepo的回答之外:
在React中,如果我们没有明确定义事件类型,那么为输入控件创建一个特殊的目标接口可能会很有用:
export interface FormControlEventTarget extends EventTarget{
    value: string;
}

然后在您的代码中,根据需要将其转换为此类型,并获得IntelliSense支持:

 import {FormControlEventTarget} from "your.helper.library"

 (event.target as FormControlEventTarget).value;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接