TypeScript 中的代码清道夫:非空断言操做符

原文地址: medium.com/better-prog…
译文地址:github.com/xiao-T/note…
本文版权归原做者全部,翻译仅用于学习。html


最近,我学到了一个很是有用的 TypeScript 的操做符:非空断言操做符。它会排除掉变量中的 null 和 undefeind。react

在这篇文章中,我将会介绍如何、什么时候使用这个操做符,并提供一些样式,但愿能够对大家有帮助。ios

TL;DR:在变量后面添加一个 ! 就会忽略 undefined 和 null。git

function simpleExample(a: number | undefined) {
   const b: number = a; // COMPILATION ERROR: undefined is not assignable to number.
   const c: number = a!; // OK
}
复制代码

如何使用非空断言操做符

非空断言操做符会从变量中移除 undefined 和 null。github

只需在变量后面添加一个 ! 便可。typescript

忽略变量的 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
}
复制代码

当函数执行时忽略 undefinedapp

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 代码同样。这可能会带来意想不到的结果。如下演示了不安全的使用方式:dom

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 打开的时候才会有效果。反之,编译器将不会检查 undefinednull函数

如今,我知道大家在想什么...

我为何要这么作?

咱们来看一下,在真实场景中非空断言操做符能有什么帮助:

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,它包含着 SpecificInputSpecificField 知道如何提取当前值,并把它传递给 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。还有,相比其余方案它还会减小你的代码量。另外,这会让你感受好像在对编译器大喊大叫,这颇有趣。

这个操做符已经帮过我不少次了。我但愿它也能给大家带来好处。

相关文章
相关标签/搜索