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。還有,相比其餘方案它還會減小你的代碼量。另外,這會讓你感受好像在對編譯器大喊大叫,這頗有趣。

這個操做符已經幫過我不少次了。我但願它也能給大家帶來好處。

相關文章
相關標籤/搜索