<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【TS】726- 編寫高效 TS 代碼的一些建議

          共 11567字,需瀏覽 24分鐘

           ·

          2020-09-25 13:11


          本文阿寶哥將分享編寫高效 TS 代碼的 5 個建議,希望這些建議對大家編寫 TS 代碼能有一些幫助。

          一、盡量減少重復(fù)代碼

          對于剛接觸 TypeScript 的小伙伴來說,在定義接口時,可能一不小心會出現(xiàn)以下類似的重復(fù)代碼。比如:

          interface?Person?{
          ??firstName:?string;
          ??lastName:?string;
          }

          interface?PersonWithBirthDate?{
          ??firstName:?string;
          ??lastName:?string;
          ??birth:?Date;
          }

          很明顯,相對于 Person 接口來說,PersonWithBirthDate 接口只是多了一個 birth 屬性,其他的屬性跟 Person 接口是一樣的。那么如何避免出現(xiàn)例子中的重復(fù)代碼呢?要解決這個問題,可以利用 extends 關(guān)鍵字:

          interface?Person?{?
          ??firstName:?string;?
          ??lastName:?string;
          }

          interface?PersonWithBirthDate?extends?Person?{?
          ??birth:?Date;
          }

          當(dāng)然除了使用 extends 關(guān)鍵字之外,也可以使用交叉運算符(&):

          type?PersonWithBirthDate?=?Person?&?{?birth:?Date?};

          另外,有時候你可能還會發(fā)現(xiàn)自己想要定義一個類型來匹配一個初始配置對象的「形狀」,比如:

          const?INIT_OPTIONS?=?{
          ??width:?640,
          ??height:?480,
          ??color:?"#00FF00",
          ??label:?"VGA",
          };

          interface?Options?{
          ??width:?number;
          ??height:?number;
          ??color:?string;
          ??label:?string;
          }

          其實,對于 Options 接口來說,你也可以使用 typeof 操作符來快速獲取配置對象的「形狀」:

          type?Options?=?typeof?INIT_OPTIONS;

          而在使用可辨識聯(lián)合(代數(shù)數(shù)據(jù)類型或標簽聯(lián)合類型)的過程中,也可能出現(xiàn)重復(fù)代碼。比如:

          interface?SaveAction?{?
          ??type:?'save';
          ??//?...
          }

          interface?LoadAction?{
          ??type:?'load';
          ??//?...
          }

          type?Action?=?SaveAction?|?LoadAction;
          type?ActionType?=?'save'?|?'load';?//?Repeated?types!

          為了避免重復(fù)定義 'save''load',我們可以使用成員訪問語法,來提取對象中屬性的類型:

          type?ActionType?=?Action['type'];?//?類型是?"save"?|?"load"

          然而在實際的開發(fā)過程中,重復(fù)的類型并不總是那么容易被發(fā)現(xiàn)。有時它們會被語法所掩蓋。比如有多個函數(shù)擁有相同的類型簽名:

          function?get(url:?string,?opts:?Options):?Promise<Response>?{?/*?...?*/?}?
          function?post(url:?string,?opts:?Options):?Promise<Response>?{?/*?...?*/?}

          對于上面的 get 和 post 方法,為了避免重復(fù)的代碼,你可以提取統(tǒng)一的類型簽名:

          type?HTTPFunction?=?(url:?string,?opts:?Options)?=>?Promise;?

          const?get:?HTTPFunction?=?(url,?opts)?=>?{?/*?...?*/?};
          const?post:?HTTPFunction?=?(url,?opts)?=>?{?/*?...?*/?};

          二、使用更精確的類型替代字符串類型

          假設(shè)你正在構(gòu)建一個音樂集,并希望為專輯定義一個類型。這時你可以使用 interface 關(guān)鍵字來定義一個 Album 類型:

          interface?Album?{
          ??artist:?string;?//?藝術(shù)家
          ??title:?string;?//?專輯標題
          ??releaseDate:?string;?//?發(fā)行日期:YYYY-MM-DD
          ??recordingType:?string;?//?錄制類型:"live"?或?"studio"
          }

          對于 Album 類型,你希望 releaseDate 屬性值的格式為 YYYY-MM-DD,而 recordingType 屬性值的范圍為 livestudio。但因為接口中 releaseDaterecordingType 屬性的類型都是字符串,所以在使用 Album 接口時,可能會出現(xiàn)以下問題:

          const?dangerous:?Album?=?{
          ??artist:?"Michael?Jackson",
          ??title:?"Dangerous",
          ??releaseDate:?"November?31,?1991",?//?與預(yù)期格式不匹配
          ??recordingType:?"Studio",?//?與預(yù)期格式不匹配
          };

          雖然 releaseDaterecordingType 的值與預(yù)期的格式不匹配,但此時 TypeScript 編譯器并不能發(fā)現(xiàn)該問題。為了解決這個問題,你應(yīng)該為 releaseDaterecordingType 屬性定義更精確的類型,比如這樣:

          interface?Album?{
          ??artist:?string;?//?藝術(shù)家
          ??title:?string;?//?專輯標題
          ??releaseDate:?Date;?//?發(fā)行日期:YYYY-MM-DD
          ??recordingType:?"studio"?|?"live";?//?錄制類型:"live"?或?"studio"
          }

          重新定義 Album 接口之后,對于前面的賦值語句,TypeScript 編譯器就會提示以下異常信息:

          const?dangerous:?Album?=?{
          ??artist:?"Michael?Jackson",
          ??title:?"Dangerous",
          ??//?不能將類型“string”分配給類型“Date”。ts(2322)
          ??releaseDate:?"November?31,?1991",?//?Error
          ??//?不能將類型“"Studio"”分配給類型“"studio"?|?"live"”。ts(2322)
          ??recordingType:?"Studio",?//?Error
          };

          為了解決上面的問題,你需要為 releaseDaterecordingType 屬性設(shè)置正確的類型,比如這樣:

          const?dangerous:?Album?=?{
          ??artist:?"Michael?Jackson",
          ??title:?"Dangerous",
          ??releaseDate:?new?Date("1991-11-31"),
          ??recordingType:?"studio",
          };

          另一個錯誤使用字符串類型的場景是設(shè)置函數(shù)的參數(shù)類型。假設(shè)你需要寫一個函數(shù),用于從一個對象數(shù)組中抽取某個屬性的值并保存到數(shù)組中,在 Underscore 庫中,這個操作被稱為 “pluck”。要實現(xiàn)該功能,你可能最先想到以下代碼:

          function?pluck(record:?any[],?key:?string):?any[]?{
          ??return?record.map((r)?=>?r[key]);
          }

          對于以上的 pluck 函數(shù)并不是很好,因為它使用了 any 類型,特別是作為返回值的類型。那么如何優(yōu)化 pluck 函數(shù)呢?首先,可以通過引入一個泛型參數(shù)來改善類型簽名:

          function?pluck<T>(record:?T[],?key:?string):?any[]?{
          ??//?Element?implicitly?has?an?'any'?type?because?expression?of?type?'string'?can't?be?used?to?
          ??//?index?type?'unknown'.
          ??//?No?index?signature?with?a?parameter?of?type?'string'?was?found?on?type?'unknown'.(7053)
          ??return?record.map((r)?=>?r[key]);?//?Error
          }

          通過以上的異常信息,可知字符串類型的 key 不能被作為 unknown 類型的索引類型。要從對象上獲取某個屬性的值,你需要保證參數(shù) key 是對象中的屬性。

          說到這里相信有一些小伙伴已經(jīng)想到了 keyof 操作符,它是 TypeScript 2.1 版本引入的,可用于獲取某種類型的所有鍵,其返回類型是聯(lián)合類型。接著使用 keyof 操作符來更新一下 pluck 函數(shù):

          function?pluck<T>(record:?T[],?key:?keyof?T)?{
          ??return?record.map((r)?=>?r[key]);
          }

          對于更新后的 pluck 函數(shù),你的 IDE 將會為你自動推斷出該函數(shù)的返回類型:

          function?pluck<T>(record:?T[],?key:?keyof?T):?T[keyof?T][]

          對于更新后的 pluck 函數(shù),你可以使用前面定義的 Album 類型來測試一下:

          const?albums:?Album[]?=?[{
          ??artist:?"Michael?Jackson",
          ??title:?"Dangerous",
          ??releaseDate:?new?Date("1991-11-31"),
          ??recordingType:?"studio",
          }];

          //?let?releaseDateArr:?(string?|?Date)[]
          let?releaseDateArr?=?pluck(albums,?'releaseDate');

          示例中的 releaseDateArr 變量,它的類型被推斷為 (string | Date)[],很明顯這并不是你所期望的,它的正確類型應(yīng)該是 Date[]。那么應(yīng)該如何解決該問題呢?這時你需要引入第二個泛型參數(shù) K,然后使用 extends 來進行約束:

          function?pluck<T,?K?extends?keyof?T>(record:?T[],?key:?K):?T[K][]?{
          ??return?record.map((r)?=>?r[key]);
          }

          //?let?releaseDateArr:?Date[]
          let?releaseDateArr?=?pluck(albums,?'releaseDate');

          三、定義的類型總是表示有效的狀態(tài)

          假設(shè)你正在構(gòu)建一個允許用戶指定頁碼,然后加載并顯示該頁面對應(yīng)內(nèi)容的 Web 應(yīng)用程序。首先,你可能會先定義 State 對象:

          interface?State?{
          ??pageContent:?string;
          ??isLoading:?boolean;
          ??errorMsg?:?string;
          }

          接著你會定義一個 renderPage 函數(shù),用來渲染頁面:

          function?renderPage(state:?State)?{
          ??if?(state.errorMsg)?{
          ????return?`嗚嗚嗚,加載頁面出現(xiàn)異常了...${state.errorMsg}`;
          ??}?else?if?(state.isLoading)?{
          ????return?`頁面加載中~~~`;
          ??}
          ??return?`
          ${state.pageContent}
          `
          ;
          }

          //?輸出結(jié)果:頁面加載中~~~
          console.log(renderPage({isLoading:?true,?pageContent:?""}));
          //?輸出結(jié)果:
          大家好,我是阿寶哥

          console.log(renderPage({isLoading:?false,?pageContent:?"大家好,我是阿寶哥"}));

          創(chuàng)建好 renderPage 函數(shù),你可以繼續(xù)定義一個 changePage 函數(shù),用于根據(jù)頁碼獲取對應(yīng)的頁面數(shù)據(jù):

          async?function?changePage(state:?State,?newPage:?string)?{
          ??state.isLoading?=?true;
          ??try?{
          ????const?response?=?await?fetch(getUrlForPage(newPage));
          ????if?(!response.ok)?{
          ??????throw?new?Error(`Unable?to?load?${newPage}:?${response.statusText}`);
          ????}
          ????const?text?=?await?response.text();
          ????state.isLoading?=?false;
          ????state.pageContent?=?text;
          ??}?catch?(e)?{
          ????state.errorMsg?=?""?+?e;
          ??}
          }

          對于以上的 changePage 函數(shù),它存在以下問題:

          • 在 catch 語句中,未把 state.isLoading 的狀態(tài)設(shè)置為 false
          • 未及時清理 state.errorMsg 的值,因此如果之前的請求失敗,那么你將繼續(xù)看到錯誤消息,而不是加載消息。

          出現(xiàn)上述問題的原因是,前面定義的 State 類型允許同時設(shè)置 isLoadingerrorMsg 的值,盡管這是一種無效的狀態(tài)。針對這個問題,你可以考慮引入可辨識聯(lián)合類型來定義不同的頁面請求狀態(tài):

          interface?RequestPending?{
          ??state:?"pending";
          }

          interface?RequestError?{
          ??state:?"error";
          ??errorMsg:?string;
          }

          interface?RequestSuccess?{
          ??state:?"ok";
          ??pageContent:?string;
          }

          type?RequestState?=?RequestPending?|?RequestError?|?RequestSuccess;

          interface?State?{
          ??currentPage:?string;
          ??requests:?{?[page:?string]:?RequestState?};
          }

          在以上代碼中,通過使用可辨識聯(lián)合類型分別定義了 3 種不同的請求狀態(tài),這樣就可以很容易的區(qū)分出不同的請求狀態(tài),從而讓業(yè)務(wù)邏輯處理更加清晰。接下來,需要基于更新后的 State 類型,來分別更新一下前面創(chuàng)建的 renderPagechangePage 函數(shù):

          更新后的 renderPage 函數(shù)

          function?renderPage(state:?State)?{
          ??const?{?currentPage?}?=?state;
          ??const?requestState?=?state.requests[currentPage];
          ??switch?(requestState.state)?{
          ????case?"pending":
          ??????return?`頁面加載中~~~`;
          ????case?"error":
          ??????return?`嗚嗚嗚,加載第${currentPage}頁出現(xiàn)異常了...${requestState.errorMsg}`;
          ????case?"ok":
          ??????`
          ${currentPage}頁的內(nèi)容:${requestState.pageContent}
          `
          ;
          ??}
          }

          更新后的 changePage 函數(shù)

          async?function?changePage(state:?State,?newPage:?string)?{
          ??state.requests[newPage]?=?{?state:?"pending"?};
          ??state.currentPage?=?newPage;
          ??try?{
          ????const?response?=?await?fetch(getUrlForPage(newPage));
          ????if?(!response.ok)?{
          ??????throw?new?Error(`無法正常加載頁面?${newPage}:?${response.statusText}`);
          ????}
          ????const?pageContent?=?await?response.text();
          ????state.requests[newPage]?=?{?state:?"ok",?pageContent?};
          ??}?catch?(e)?{
          ????state.requests[newPage]?=?{?state:?"error",?errorMsg:?""?+?e?};
          ??}
          }

          changePage 函數(shù)中,會根據(jù)不同的情形設(shè)置不同的請求狀態(tài),而不同的請求狀態(tài)會包含不同的信息。這樣 renderPage 函數(shù)就可以根據(jù)統(tǒng)一的 state 屬性值來進行相應(yīng)的處理。因此,通過使用可辨識聯(lián)合類型,讓請求的每種狀態(tài)都是有效的狀態(tài),不會出現(xiàn)無效狀態(tài)的問題。

          四、選擇條件類型而不是重載聲明

          假設(shè)你要使用 TS 實現(xiàn)一個 double 函數(shù),該函數(shù)支持 stringnumber 類型。這時,你可能已經(jīng)想到了使用聯(lián)合類型和函數(shù)重載:

          function?double(x:?number?|?string):?number?|?string;
          function?double(x:?any)?{
          ??return?x?+?x;
          }

          雖然這個 double 函數(shù)的聲明是正確的,但它有一點不精確:

          //?const?num:?string?|?number
          const?num?=?double(10);?
          //?const?str:?string?|?number
          const?str?=?double('ts');

          對于 double 函數(shù),你期望傳入的參數(shù)類型是 number 類型,其返回值的類型也是 number 類型。當(dāng)你傳入的參數(shù)類型是 string 類型,其返回的類型也是 string 類型。而上面的 double 函數(shù)卻是返回了 string | number 類型。為了實現(xiàn)上述的要求,你可能想到了引入泛型變量和泛型約束:

          function?double<T?extends?number?|?string>(x:?T):?T;
          function?double(x:?any)?{
          ??return?x?+?x;
          }

          在上面的 double 函數(shù)中,引入了泛型變量 T,同時使用 extends 約束了其類型范圍。

          //?const?num:?10
          const?num?=?double(10);
          //?const?str:?"ts"
          const?str?=?double('ts');
          console.log(str);

          不幸的是,我們對精確度的追求超過了預(yù)期。現(xiàn)在的類型有點太精確了。當(dāng)傳遞一個字符串類型時,double 聲明將返回一個字符串類型,這是正確的。但是當(dāng)傳遞一個字符串字面量類型時,返回的類型是相同的字符串字面量類型。這是錯誤的,因為 ts 經(jīng)過 double 函數(shù)處理后,返回的是 tsts,而不是 ts

          另一種方案是提供多種類型聲明。雖然 TypeScript 只允許你編寫一個具體的實現(xiàn),但它允許你編寫任意數(shù)量的類型聲明。你可以使用函數(shù)重載來改善 double 的類型:

          function?double(x:?number):?number;
          function?double(x:?string):?string;
          function?double(x:?any)?{
          ??return?x?+?x;
          }

          //?const?num:?number
          const?num?=?double(10);?
          //?const?str:?string
          const?str?=?double("ts");?

          很明顯此時 num 和 str 變量的類型都是正確的,但不幸的是,double 函數(shù)還有一個小問題。因為 double 函數(shù)的聲明只支持 stringnumber 類型的值,而不支持 string | number 聯(lián)合類型,比如:

          function?doubleFn(x:?number?|?string)?{
          ??//?Argument?of?type?'string?|?number'?is?not?assignable?to?
          ??//?parameter?of?type?'number'.
          ??//?Argument?of?type?'string?|?number'?is?not?assignable?to?
          ??//?parameter?of?type?'string'.
          ??return?double(x);?//?Error
          }

          為什么會提示以上的錯誤呢?因為當(dāng) TypeScript 編譯器處理函數(shù)重載時,它會查找重載列表,直到找一個匹配的簽名。對于 number | string 聯(lián)合類型,很明顯是匹配失敗的。

          然而對于上述的問題,雖然可以通過新增 string | number 的重載簽名來解決,但最好的方案是使用條件類型。在類型空間中,條件類型就像 if 語句一樣:

          function?double<T?extends?number?|?string>(
          ??x:?T
          ):?T?extends?string???string?:?number
          ;
          function?double(x:?any)?{
          ??return?x?+?x;
          }

          這與前面引入泛型版本的 double 函數(shù)聲明類似,只是它引入更復(fù)雜的返回類型。條件類型使用起來很簡單,與 JavaScript 中的三目運算符(?:)一樣的規(guī)則。T extends string ? string : number 的意思是,如果 T 類型是 string 類型的子集,則 double 函數(shù)的返回值類型為 string 類型,否則為 number 類型。

          在引入條件類型之后,前面的所有例子就可以正常工作了:

          //?const?num:?number
          const?num?=?double(10);?
          //?const?str:?string
          const?str?=?double("ts");?

          //?function?f(x:?string?|?number):?string?|?number
          function?f(x:?number?|?string)?{
          ??return?double(x);
          }

          五、一次性創(chuàng)建對象

          在 JavaScript 中可以很容易地創(chuàng)建一個表示二維坐標點的對象:

          const?pt?=?{};?
          pt.x?=?3;?
          pt.y?=?4;

          然而對于同樣的代碼,在 TypeScript 中會提示以下錯誤信息:

          const?pt?=?{};
          //?Property?'x'?does?not?exist?on?type?'{}'
          pt.x?=?3;?//?Error
          //?Property?'y'?does?not?exist?on?type?'{}'
          pt.y?=?4;?//?Error

          這是因為第一行中 pt 變量的類型是根據(jù)它的值 {} 推斷出來的,你只能對已知的屬性賦值。針對這個問題,你可能會想到一種解決方案,即新聲明一個 Point 類型,然后把它作為 pt 變量的類型:

          interface?Point?{
          ??x:?number;
          ??y:?number;
          }

          //?Type?'{}'?is?missing?the?following?properties?from?type?'Point':?x,?y(2739)
          const?pt:?Point?=?{};?//?Error
          pt.x?=?3;
          pt.y?=?4;

          那么如何解決上述問題呢?其中一種最簡單的解決方案是一次性創(chuàng)建對象:

          const?pt?=?{?
          ??x:?3,
          ??y:?4,?
          };?//?OK

          如果你想一步一步地創(chuàng)建對象,你可以使用類型斷言(as)來消除類型檢查:

          const?pt?=?{}?as?Point;?
          pt.x?=?3;
          pt.y?=?4;?//?OK

          但是更好的方法是一次性創(chuàng)建對象并顯式聲明變量的類型:

          const?pt:?Point?=?{?
          ??x:?3,
          ??y:?4,?
          };

          而當(dāng)你需要從較小的對象來構(gòu)建一個較大的對象時,你可能會這樣處理,比如:

          const?pt?=?{?x:?3,?y:?4?};
          const?id?=?{?name:?"Pythagoras"?};
          const?namedPoint?=?{};
          Object.assign(namedPoint,?pt,?id);

          //?Property?'id'?does?not?exist?on?type?'{}'.(2339)
          namedPoint.name;?//?Error

          為了解決上述問題,你可以使用對象展開運算符 ... 來一次性構(gòu)建大的對象:

          const?namedPoint?=?{...pt,?...id};?
          namedPoint.name;?//?OK,?type?is?string

          此外,你還可以使用對象展開運算符,以一種類型安全的方式逐個字段地構(gòu)建對象。關(guān)鍵是在每次更新時使用一個新變量,這樣每個變量都會得到一個新類型:

          const?pt0?=?{};
          const?pt1?=?{...pt0,?x:?3};
          const?pt:?Point?=?{...pt1,?y:?4};?//?OK

          雖然這是構(gòu)建這樣一個簡單對象的一種迂回方式,但對于向?qū)ο筇砑訉傩圆⒃试S TypeScript 推斷新類型來說,這可能是一種有用的技術(shù)。要以類型安全的方式有條件地添加屬性,可以使用帶 null{} 的對象展開運算符,它不會添加任何屬性:

          declare?var?hasMiddle:?boolean;
          const?firstLast?=?{first:?'Harry',?last:?'Truman'};
          const?president?=?{...firstLast,?...(hasMiddle???{middle:?'S'}?:?{})};

          如果在編輯器中鼠標移到 president,你將看到 TypeScript 推斷出的類型:

          const?president:?{
          ??middle?:?string;
          ??first:?string;
          ??last:?string;
          }

          最終通過設(shè)置 hasMiddle 變量的值,你就可以控制 president 對象中 middle 屬性的值:

          declare?var?hasMiddle:?boolean;
          var?hasMiddle?=?true;
          const?firstLast?=?{first:?'Harry',?last:?'Truman'};
          const?president?=?{...firstLast,?...(hasMiddle???{middle:?'S'}?:?{})};

          let?mid?=?president.middle
          console.log(mid);?//?S

          六、參考資源

          • effective-typescript-specific-ways-improve
          推薦閱讀
          「1.8W字」2020不可多得的 TS 學(xué)習(xí)指南

          「1.8W字」2020不可多得的 TS 學(xué)習(xí)指南

          細數(shù)這些年被困擾過的 TS 問題

          細數(shù)這些年被困擾過的 TS 問題

          細數(shù) TS 中那些奇怪的符號

          細數(shù) TS 中那些奇怪的符號

          聚焦全棧,專注分享 TypeScript、Web API、Deno 等技術(shù)干貨。

          瀏覽 56
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  超碰自拍网站 | 久久天堂影院 | 亚洲一级一射欧美999 | 亚洲一级天堂 | 日本一级片免费观看 |