<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 代碼的一些建議

          共 17322字,需瀏覽 35分鐘

           ·

          2022-11-24 23:33


          感謝阿寶哥分享高效 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)鍵字之外,也可以使用交叉運(yù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;
          }

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

          type Options = typeof INIT_OPTIONS;

          而在使用可辨識聯(lián)合(代數(shù)數(shù)據(jù)類型或標(biāo)簽聯(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"

          然而在實(shí)際的開發(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<Response>; 

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

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

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

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

          對于 Album 類型,你希望 releaseDate 屬性值的格式為 YYYY-MM-DD,而 recordingType 屬性值的范圍為 livestudio。但因?yàn)榻涌谥?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// 專輯標(biāo)題
            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”。要實(shí)現(xiàn)該功能,你可能最先想到以下代碼:

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

          對于以上的 pluck 函數(shù)并不是很好,因?yàn)樗褂昧?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 來進(jìn)行約束:

          function pluck<TK 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 `<div>${state.pageContent}</div>`;
          }

          // 輸出結(jié)果:頁面加載中~~~
          console.log(renderPage({isLoading: true, pageContent: ""}));
          // 輸出結(jié)果:<div>大家好,我是阿寶哥</div>
          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":
                `<div>第${currentPage}頁的內(nèi)容:${requestState.pageContent}</div>`;
            }
          }

          更新后的 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 屬性值來進(jìn)行相應(yīng)的處理。因此,通過使用可辨識聯(lián)合類型,讓請求的每種狀態(tài)都是有效的狀態(tài),不會出現(xiàn)無效狀態(tài)的問題。

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

          假設(shè)你要使用 TS 實(shí)現(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ù)的聲明是正確的,但它有一點(diǎn)不精確:

          // 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 類型。為了實(shí)現(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)在的類型有點(diǎn)太精確了。當(dāng)傳遞一個字符串類型時,double 聲明將返回一個字符串類型,這是正確的。但是當(dāng)傳遞一個字符串字面量類型時,返回的類型是相同的字符串字面量類型。這是錯誤的,因?yàn)?ts 經(jīng)過 double 函數(shù)處理后,返回的是 tsts,而不是 ts

          另一種方案是提供多種類型聲明。雖然 TypeScript 只允許你編寫一個具體的實(shí)現(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ù)還有一個小問題。因?yàn)?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
          }

          為什么會提示以上的錯誤呢?因?yàn)楫?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 中的三目運(yùn)算符(?:)一樣的規(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)建一個表示二維坐標(biāo)點(diǎn)的對象:

          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

          這是因?yàn)榈谝恍兄?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

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

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

          此外,你還可以使用對象展開運(yùn)算符,以一種類型安全的方式逐個字段地構(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{} 的對象展開運(yùn)算符,它不會添加任何屬性:

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

          如果在編輯器中鼠標(biāo)移到 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

          往期精彩


          瀏覽 28
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  青娱乐国产分类日韩成人 | 国产精品女人久久久 | 三级片中文字幕在线观看 | 啊使劲用力操骚逼啊视频 | 亚洲色情在线视频 |