原文地址: medium.com/better-prog…
译文地址:github.com/xiao-T/note…
本文版权归原作者所有,翻译仅用于学习。
最近,我学到了一个非常有用的 TypeScript 的操作符:非空断言操作符。它会排除掉变量中的 null 和 undefeind。
在这篇文章中,我将会介绍如何、何时使用这个操作符,并提供一些样式,希望可以对你们有帮助。
TL;DR:在变量后面添加一个 ! 就会忽略 undefined 和 null。
function simpleExample(a: number | undefined) {
const b: number = a; // COMPILATION ERROR: undefined is not assignable to number.
const c: number = a!; // OK
}
如何使用非空断言操作符
非空断言操作符会从变量中移除 undefined 和 null。
只需在变量后面添加一个 ! 即可。
忽略变量的 undefined | null
:
function myFunc(maybeString: string | undefined | null) {
const onlyString: string = maybeString; //compilation error: string | undefined | null is not assignable to string
const ignoreUndefinedAndNull: string = maybeString!; //no problem
}
当函数执行时忽略 undefined
:
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
const num1 = numGenerator(); //compilation error: cannot invoke an object which is possibly undefined
const num2 = numGenerator!(); //no problem
}
当断言操作符在 runtime 失败时,代码就跟正常的 JavaScript 代码一样。这可能会带来意想不到的结果。以下演示了不安全的使用方式:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);// prints undefined, although b’s type does not include undefined
-------
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
const num1 = numGenerator!();
}
myFunc(undefined); // runtime error: Uncaught TypeError: numGenerator is not a function
请注意:这个操作符只有在 strictNullChecks
打开的时候才会有效果。反之,编译器将不会检查 undefined
和 null
现在,我知道你们在想什么...
我为什么要这么做?
我们来看一下,在真实场景中非空断言操作符能有什么帮助:React refs 的事件处理
React refs 可以用来访问 HTML 节点 DOM。ref.current 的值有时可能是 null(这是因为引用的元素还没有 mounted)。
在很多情况下,我们能确定 current 元素已经 mounted,因此,null
是不需要的。
在接来下的示例中,当点击按钮时 input 会滚动到可视区域:
const ScrolledInput = () => {
const ref = React.createRef<HTMLInputElement>();
const goToInput = () => ref.current.scrollIntoView(); //compilation error: ref.current is possibly null
return (
<div>
<input ref={ref}/>
<button onClick={goToInput}>Go to Input</button>
</div>
);
};
我们知道当 goToInput
执行时,input 元素一定是 mounted。我们可以大胆假设 ref.current
是非空的:
//...
const goToInput = () => ref.current!.scrollIntoView(); //all good!
//...
不使用断言操作符我们也可以解决编译错误的问题。我们可以使用逻辑与
//...
const goToInput = () => ref.current && ref.current.scrollIntoView();
//...
这就有点啰嗦了。
当链式调用可选属性时,就会变得特别麻烦。我们来看一个极端的演示:
// Logical AND
object && object.prop && object.prop.func && object.prop.func();
// Compared to the Non-null assertion operator:
object!.prop!.func!(); //assuming object.prop.func are all defined
React 中的 prop 注入测试
接下来的示例是一种常见的 prop 测试模式。我们会有两个组件。
第一个是可重用的组件,它也可以是第三方组件。它有一个 callback prop,当 input 的 value 改变时会调用这个 callback。Callback 会接受一个 event 参数。
interface SpecialInputProps {
onChange?: (e: React.FormEvent<HTMLInputElement>) => void;
}
const SpecialInput = (s: SpecialInputProps) => <input onChange={s.onChange}/>;
第二个组件叫做 SpecificField
,它包含着 SpecificInput
。SpecificField
知道如何提取当前值,并把它传递给 callback:
interface SpecificFieldProps {
onFieldChanged: (newValue: string) => void;
}
const SpecificField = (props: SpecificFieldProps) => {
const inputCallback = (e: React.FormEvent<HTMLInputElement>) => props.onFieldChanged(e.currentTarget.value);
return <SpecialInput onChange={inputCallback}/>;
};
我们想测试 SpecificField
是否可以正确的调用 onChange
。
也就是说 SpecificField
的回调 onFieldChanged
是否可以得到一个 event.currentTarget.value:
//SpecificField.test.tsx
it('should inject callback to SpecialInput which calls the onFieldChanged callback with the event value', () => {
const onFieldChanged = jest.fn();
const wrapper = shallow(<SpecificField onFieldChanged={onFieldChanged}/>);
const injectedCallback = wrapper.find(SpecialInput).props().onChange;
expect(injectedCallback).toBeDefined();
const event = {currentTarget: {value: "new value"}} as React.FormEvent<HTMLInputElement>;
injectedCallback(event); //compilation error: cannot invoke an object which is possibly undefined
expect(onFieldChanged).toHaveBeenCalledWith("new value");
});
因为 onFieldChanged
是可选的,因此可能是 undefined,这就会引起编译错误。
当然,我们知道 injectedCallback
已经被定义了。 — 我还测试过它。
这时,非空断言操作符就可以帮助我们:
//...
injectedCallback!(event); //no problem
//...
另外一个解决方案,我们不使用断言操作符,而是,使用 if-else 条件语句:
if (injectedCallback) {
injectedCallback(event); //injectedCallback has to be defined in the “if” block
expect(onFieldChanged).toHaveBeenCalledWith("new value");
} else {
fail("SpecialInput was not injected with a callback as expected");
}
这非常啰嗦。
两者之间,我更喜欢非空断言操作符。它更加简短、清晰,没有多余的代码。
总结
非空断言操作符是一个非常实用的工具。使用的时候要小心处理。滥用将会带来意想不到的结果。
然而,好处还是很多的: It reduces cognitive load in certain scenarios that cannot happen in runtime。还有,相比其他方案它还会减少你的代码量。另外,这会让你感觉好像在对编译器大喊大叫,这很有趣。
这个操作符已经帮过我很多次了。我希望它也能给你们带来好处。