React: React With TypeScript

20.08.13

Photo by Dee @ Copper and Wild on Unsplash

create-react-app 官方有個 TypeScript 初始化懶人包,我們將透過 command line 下載懶人包,並將專案命名為 typescript-react-textbook。

$ npx create-react-app typescript-react-textbook --template typescript
$ cd typescript-react-textbook

對熟悉React的人來說,TypeScript懶人包的資料夾結構與一般的js懶人包沒有太大的差別。除了

.js檔變成.tsx檔 多了一個typescript設定檔tsconfig.json

在做任何改動之前,先把專案跑起來,看能否正常啟動:

$ yarn start || npm start

自訂一個元件

接下來要練習,如何用TypeScript製作一個共用元件,並在App.tsx當中使用。 以下會將游標框起來的部分,做成一個共用的<Link />元件。

src目錄下新建一個Components的資料夾,並新增Link.tsx的檔案。 我們先不管型別,一如往常的新建一個functional component。

/*
 * src/Components/Link.tsx
 */

import React from 'react';

const Link = ({
	href,
	content
}) => {
  return (
    <a
      className="App-link"
      href={href}
      target="_blank"
      rel="noopener noreferrer"
    >
      {content}
    </a>
  );
};

export default Link;

此時編輯器可能會出現紅色毛毛蟲的警示線,我們先不要理它。 回到App.tsx,import 這個元件。

Oops,只是import而已,瀏覽器就跳出錯誤訊息。

預設的型別:any

Binding element ‘href’ implicitly has an ‘any’ type.

意思是:你尚未指定href這個props的型別,你是否預設它的型別是any?

any是TypeScript的型別之一。不論這個變數是什麼,我都吃! 不過any根本違反了使用強型別的理由,除非你在定義時說清楚「我就是要用any」,否則TypeScript會跳出錯誤來提醒你。

我們確實可以敷衍過去,將所有的型別設為any,編譯器也會放你一馬。

/*
 * src/Components/Link.tsx
 */

const Link = (props: {
    href: any;    content: any;}) => {
  return (
    <a
      className="App-link"
      href={props.href}
      target="_blank"
      rel="noopener noreferrer"
    >
      {props.content}
    </a>
  );
};

等等,既然大家都是any,何苦學TypeScript來為難自己呢 T_T?

any存在的意義,是讓人在萬不得已的情況下使用。例如有些變數,我們無法預期它會接到什麼值。

const param = JSON.parse(someData);

既然要使用TypeScript,除了特殊案例以外,就請確實的定義型別吧!

/*
 * src/Components/Link.tsx
 */

const Link = (props: {
    href: string;    content: string;}) => {
  return (
    <a
      className="App-link"
      href={props.href}
      target="_blank"
      rel="noopener noreferrer"
    >
      {props.content}
    </a>
  );
};

React內建的props怎麼定義型別

props.children是我們在建立元件時,很常用到的變數。 children怎麼定義型別呢?React官方已經幫我們準備好了。 以Functional Component來說,官方有提供React.FunctionComponent<Props>方便開發者使用, 可以簡寫為React.FC<Props>。 在React.FC<Props>當中,官方已經幫我們定義好children, displayName, defaultProps等屬性的型別。 只要標示元件的型別為React.FC<Props>,即可省去定義這些預設屬性的麻煩。

/*
 * src/Components/Link.tsx
 */

const Link: React.FC<{    href: string;
    content: string;
}> = (props) => {

    const { children } = props;    
    return (
        <a
          className="App-link"
          href={props.href}
          target="_blank"
          rel="noopener noreferrer"
        >
          {props.content}
        </a>
    );
};

對熟悉強型別語言的人來說,<>一點都不陌生,就是泛用型別,簡稱泛型。 它的使用情境是:這個Function所帶入的參數,型別可能有所不同。 有時候參數是number,也可能是string,某一天也可能是Object。 反正我不想把這個變數的型別定死,用到的時候再設定它的型別就可以了。 使用者只要在<>角括號裡面,填入你這次要檢查的型別即可。 例如<string><number>等等。

// Type = set_the_Type_you_want
function aFuncReturnInput<Type>(input: Type): Type {  return input;
}

const isNumber = aFuncReturnInput<number>(30);
const isString = aFuncReturnInput<string>('A Loooooooooog Word');

因此,const Link: React.FC<{}>的意思是:我宣告了一個React元件Link,其參數(props)的型別是Object。 在Object裡面,再去詳細定義有哪些必要 / 非必要的key-value。

const Link: React.FC<{    href: string;    content: string;}> = (props) => {
    return ();
}

抽象化的型別 Type Alias

如過你覺得props的key實在太多了,程式碼不美觀,也可以採用”抽象化型別”的方式來宣告props。

type LinkProps = {	href: string;	content: string;};
const Link: React.FC<LinkProps> = (props) => {
  return ();
}

使用元件

到目前為止,我們終於用Typescript完成一個基礎元件。 回到一開始的目標,我們要將HTML的<a>標籤,取代成自訂的<Link>元件。

之前提到,Typescript會檢查必填/非必填的屬性,若是少了必填的屬性,編輯器會出現紅色毛毛蟲提醒你。

Summary

此章節學到了:

  1. 在尚未定義型別之前,Typescript預設的隱含型別為any
  2. 宣告Function的方式
  3. 宣告物件的方式
  4. 使用React.FC<Props>來宣告Functional Component
  5. 將常用的物件抽象化,自訂型別

附件 Attachment

型別宣告

宣告 - 物件

宣告一個”貓”物件,必填屬性有3個。分別是name,gender,age:

const cat: { name: string; gender: string; age: number } = {  name: 'mimi',
  gender: 'female',
  age: 3
};

宣告一個”貓”物件,屬性name,gender為必填,age為非必填:

const cat: { 
    name: string;
    gender: string;
    age?: number; // 在 key 後面加上 ? 表示該屬性非必填} = {
  name: 'mimi',
  gender: 'female'
};

宣告 - Function

宣告一個函式plus,有兩個參數,分別為x, y。x, y的型別皆為數字。 plus會回傳x + y的總和,回傳值也是數字:

const plus = (x: number, y: number): number => {  return x + y;
};
plus(5, 5); // 10

將上面的函式plus,改成只有一個參數obj。 obj是一個物件,有兩個key,分別為x, y。 回傳值同樣為x + y的總和:

const plus = (obj: {    x: number;    y: number;}): number => {  return x + y;
};
plus(5, 5); // 10

同上,將參數以解構賦值(object destructuring assignment)的方式來撰寫:

const plus = ({x, y}: { // 將obj直接展開為{x, y}    x: number;
    y: number;
}): number => {
  return x + y;
};
plus(5, 5); // 10

有些function只是執行某個工作,並不會有回傳值。 沒有回傳值時,宣告的方法如下:

const doSomething = ({x, y}: {
    x: number;
    y: number;
}): void => { // 沒有回傳值: void    console.log(x, y);
}
doSomething({5, 5});

宣告 - 自訂抽象化的型別 Type Alias

當專案越來越大的時候,我們會發現,有些屬性會重複出現,例如:

當其他物件要使用同一個商品,我們又要把所有屬性複製貼上:

Typescript可以把重複的部分抽出來,自己定義一個型別。

type Book = {
  title: string;
  author: string;
  publicationDate: string;
  publisher: string;
  cover: string;
  editor: string;
};

這個型別即可重複使用。如果哪天,商品的欄位增加了,會更容易修改程式碼。

const Item: React.FC<Book> = (props) => {  return (
    <div className="item"></div>
  );
};