iT邦幫忙

1

(已自行解決)關於新手入門React+TypeScript遇到的困擾⋯(文長)

  • 分享至 

  • xImage

前言
學完React一陣子之後也寫了一個網站的小專案,想說現在很多公司都會用到TypeScript(以下簡稱TS)來編寫,於是自己也開始學習(自己是自學,沒有在學校或出去補習班上過課),目前已經將基本的用法、包括泛型甚至Utility Type的一些工具用法都學完了,自己TS的學習來源是w3school網站(當然遇到更深的問題也會直接google找),但一直沒解決我遇到的問題,因此想在這裡詢問看看各位先進的建議或想法,由於是IT邦初學者,可能文章內容多有拙劣之處,還請多指教QQ

進入正題

最近一直有一個困擾,在用TS的時候,不知道什麼時候該寫型別,何時不用寫,因為TS本身有推論這個方法會幫我們自動識別型別,也就造成我不曉得什麼時候需要寫型別。
最一開始是想說,從"如果沒報錯,那我就不特地寫"的心態,到"如果都讓他推論,且都正確,那TS的寫型別的初衷以及我自己沒有練習到定義型別"的這兩個問題,浮現於心頭,於是才正式開始練習自己寫型別。

但寫久了,就會像上面說的,自己處於一個很曖昧的感覺,該寫不該寫,尤其遇到使用第三方庫(包)的時候,舉例目前遇到很頭痛的但又很常使用的:axios,相信各位都有使用的經驗。
而自己已知axios它本身有附帶宣告型別的文件,但是搞不清楚哪個變數、函數或參數甚至返回值的內容該用它給的宣告型別中的哪個型別,也有去特地查了模擬axios創建至請求完畢的那種影片來看,也回頭看了promise的整個流程的教學,當然axios裡附帶的宣告型別的文件也重複看了好幾次,但,還是一知半解...。

因此想說自己練習寫一個專門去獲取資料的組件,看能不能更加了解。
於是參考了一位外國youtuber的影片:https://youtu.be/fTFoBr5LJGE

基本上是模仿它的內容在寫的(不過他是用js寫非TS),但我不想寫成api的形式,我把請求的函數寫成一個組件。
然後會想練習他寫的這個小項目是因為,我也想使用React中的Suspense去自動切換網頁顯示的畫面(loading中 or 獲取資料成功 => 展示資料)。

以下慢慢說明文件各項內容。

首先我將想要請求的數據,分成兩個json檔案,分別是works.json以及education.json,裡面都是對象的多層結構,如下:

education.json

{
  "title": {
    "name": "Education",
    "svgClass": "bi bi-mortarboard-fill",
    "path": {
      "1": "M8.211 2.047a.5.5 0 0 0-.422 0l-7.5 3.5a.5.5 0 0 0 .025.917l7.5 3a.5.5 0 0 0 .372 0L14 7.14V13a1 1 0 0 0-1 1v2h3v-2a1 1 0 0 0-1-1V6.739l.686-.275a.5.5 0 0 0 .025-.917l-7.5-3.5Z",
      "2": "M4.176 9.032a.5.5 0 0 0-.656.327l-.5 1.7a.5.5 0 0 0 .294.605l4.5 1.8a.5.5 0 0 0 .372 0l4.5-1.8a.5.5 0 0 0 .294-.605l-.5-1.7a.5.5 0 0 0-.656-.327L8 10.466 4.176 9.032Z"
    }
  },
  "college": {
    "school_en": "XX University",
    "school_cn": "XX大學",
    "year": "2009 - 2013",
    "status": "Department of Economics, Bachelor Degree"
  },
  "high_school": {
    "school_en": "XXX High School",
    "school_cn": "台北市立XX高中",
    "year": "2006 - 2009",
    "status": "Graduated"
  }
}

works.json

{
  "title": {
    "name": "Work Experience",
    "svgClass": "bi bi-briefcase-fill",
    "path": {
      "1": "M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v1.384l7.614 2.03a1.5 1.5 0 0 0 .772 0L16 5.884V4.5A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zm0 1h3a.5.5 0 0 1 .5.5V3H6v-.5a.5.5 0 0 1 .5-.5z",
      "2": "M0 12.5A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5V6.85L8.129 8.947a.5.5 0 0 1-.258 0L0 6.85v5.65z"
    }
  },
  "XXX": {
    "company": "XXX公司",
    "position": "專案助理",
    "year": "Apr 2019 - Nov 2021",
    "location": "新北市新莊區",
    "industry": "國際貿易業",
    "descriptions": {
      "1": "工作內容1",
      "2": "工作內容2",
      "3": "工作內容3"
    }
  },
  "OOO": {
    "company": "OOO股份有限公司",
    "position": "業務助理",
    "year": "Aug 2018 - Dec 2018",
    "location": "基隆市",
    "industry": "房地產經營與租賃業",
    "descriptions": {
      "1": "工作內容1",
      "2": "工作內容2",
      "3": "工作內容3"
    }
  },
  "OXOX": {
    "company": "OXOX企業",
    "position": "行政人員",
    "year": "Apr 2018 - Jul 2018",
    "location": "台北市大安區",
    "industry": "餐飲業",
    "descriptions": {
      "1": "工作內容1",
      "2": "工作內容2",
      "3": "工作內容3"
    }
  }
}

svgClass以及path的部分分別是:SVG圖片的className值、SVG圖片的path標籤中的d屬性裡的值。

於是我創建了一個叫做FetchData的組件,它的功能主要就如上所說,用於獲取資料,請求URL則是欲使用此請求組件的上層組件透過props傳遞url進來的。

以下為該組件(目前)全部代碼(只寫到TS漂紅報錯我就先停住沒往下寫了),還請先看代碼。

FetchData.tsx

import axios, { AxiosError, AxiosResponse } from 'axios'

type Props = { url: string }  // 定義傳入的props型別(主要傳進來的東西只有url,因此暫時只寫這樣)。

// 由於兩個json中title屬性(對象)的內容是一樣的,於是在這裡一次定義,供兩者共同使用此型別。
interface Title<S = string> {
  name: S
  svgClass: S  // SVG圖片的className值。
  path: {[key: string]: S}  // SVG圖片的path標籤中的d屬性裡的值。
}

// 定義works.json裡扣除key值為"title"的對象以外的其餘部分之型別。
interface Work<S = string> {
  company: S
  position: S
  year: S
  location: S
  industry: S
  descriptions: {[key: string]: S}
}

// 定義education.json裡扣除key值為"title"的對象以外的其餘部分之型別。
interface Education<S = string> {
  school_en: S
  school_cn: S
  year: S
  status: S
}

// 定義checkStatus函數的型別
type CheckStatus = (promise: Promise<unknown> | Promise<AxiosResponse<Data>>) => {
  read: () => string | AxiosResponse<Data, any> | AxiosError<unknown, any>;
}

// 定義請求後返回的資料內容之型別
export type Data = {[key: string]: Title | Education | Work}

const FetchData = ({ url }: Props) => {

  // 此函數用於發送請求獲取資料
  const getData: () => Promise<unknown> | Promise<AxiosResponse<Data>> = async () => {
    try {
      const res: AxiosResponse<Data> = await axios(url)
      return res.data
    } catch (err) {
      return err
    }
  }

  // 此函數用於檢查promise的state(進行狀態到哪了:pending?success?error?)
  const checkStatus: CheckStatus = (promise: Promise<AxiosResponse<Data>>) => {
    let status = 'pending'
    let result: string | AxiosResponse<Data> | AxiosError = ''
    let suspender = promise.then((r: AxiosResponse<Data>) => {
      status = 'success'
      result = r
    }, (e: AxiosError) => {
      status = 'error'
      result = e
    })
    /*
     接著下面同樣是在checkStatus函數中,return一個對象,對象中有個read()函數,為了讓上層組件(使用FetchData組件獲取資料的組件:ShowData組件),可以調用read()去讓React的Suspense偵測資料回沒回來(是否已經獲取完畢?),若還沒,則顯示Loading組件,若回來,則正常展示數據給用戶。
    */
    return {
      read: () => {
        if (status === 'pending') {
          throw suspender
        } else if (status === 'error') {
          throw result
        }
        return result
      }
    }
  }  // checkStatus函數的底部到上面這個return結束(由於排版不好看清楚,在此特別註明)。


  return {
    data: checkStatus(getData())
  }
}

export default FetchData


/*
 但寫到這裡,checkStatus函數就漂紅了(報錯...),警告如下:
  類型 '(promise: Promise<AxiosResponse<Data>>) => { 
    read: () => string | AxiosResponse<Data, any> | AxiosError<unknown, any>;
  }' 不可指派給類型 'CheckStatus'。
  參數 'promise' 和 'promise' 的類型不相容。
  類型 'Promise<unknown> | Promise<AxiosResponse<Data, any>>' 
  不可指派給類型'Promise<AxiosResponse<Data, any>>'。
  類型 'Promise<unknown>' 不可指派給類型 'Promise<AxiosResponse<Data, any>>'。
  類型 'unknown' 不可指派給類型 'AxiosResponse<Data, any>'。ts(2322)
*/

在ShowData組件中使用FetchData組件時,會在外面包一層React的Suspense,讓資料還沒回來時,有loading畫面,但這邊還沒寫完整個內容,因此先暫時寫上偽代碼,讓大家比較容易有個畫面。

ShowData.tsx

import { AxiosError, AxiosResponse } from "axios";
import { Data } from "../helpers/FetchData";

// 定義read()函數的型別
type Read = () => string | AxiosResponse<Data, any> | AxiosError<unknown, any>

// 定義傳入的props內容之型別
type ShowDataProps = {
  resource: {
    data: Read
  }
}

const ShowData = (props: ShowDataProps) => {

  const {resource} = props
  
  /* 這裡的resource.data.read()中的read()也漂紅報錯,警告:類型 'Read' 沒有屬性 'read'。ts(2339) */
  const data = resource.data.read()
  
  
 /* return裡面打算用來展示得到的資料(暫時先寫個data.title.name當作返回資料的內容,請大家腦內暫時有個畫面就好) */
  return (
    <div>
      {data.title.name}
    </div>
  )
}

export default ShowData

然後是App.tsx,雖然還沒完成整個內容,但先在此寫上偽代碼QQ

App.tsx

import { Suspense } from 'react';
import Loading from './components/Loading'
import FetchData from './helpers/FetchData';
import ShowData from './components/ShowData';

const resource = FetchData({url: "/data/works.json"})

const App = () => {

/* 
 下面<ShowData resource={resource} />中,resource屬性的部分也漂紅報錯,
 警告:
 類型 '{ data: { read: () => string | AxiosResponse<Data, any> | AxiosError<unknown, any>; }; }' 不可指派給類型 '{ data: Read; }'。
 屬性 'data' 的類型不相容。
 類型 '{ read: () => string | AxiosResponse<Data, any> | AxiosError<unknown, any>; }' 不可指派給類型 'Read'。
 類型 '{ read: () => string | AxiosResponse<Data, any> | AxiosError<unknown, any>; }' 沒有符合特徵標記 '(): string | AxiosResponse<Data, any> | AxiosError<unknown, any>' 的項目。ts(2322)
    ShowData.tsx(7, 3): 所需類型來自屬性 'resource',該屬性宣告於此處的類型 'IntrinsicAttributes &   ShowDataProps' 上
 */
  return (
    <div>
      <Suspense fallback={<Loading />}>
        <ShowData resource={resource} />
      </Suspense>
    </div>
  );
}

export default App;

以上就是我能寫到的目前全部代碼,看到這裡的大家辛苦了,新手寫的代碼品質粗糙還請見諒。

然後,我寫到這就卡住了...,不知道FetchData組件中的axios相關的型別我要怎麼寫,報錯的原因看了警告也不是太懂,然後應該怎麼修正、哪個東西該用哪個型別,以及接下來該怎麼寫下去,感覺寫得一團糟QQ,還請各位在下方留言解惑,先謝謝各位先進前輩們。

後記
新手發問:不知道怎麼把發問文章弄成已解決,是不是需要選個人做為最佳解答阿?
但是沒有看到任何可以按的選項QQ",只好先在文章標題加上已解決。

看更多先前的討論...收起先前的討論...
froce iT邦大師 1 級 ‧ 2022-12-08 08:38:09 檢舉
就你能確定的寫,不能確定的不要寫型別。
假設這個API不是你能控制的,並且他文件也沒寫完整,那你也沒辦法寫出他完整形別啊。
另外有些時候你也沒辦法確定一個libary的執行結果會吐給你什麼,那也只能不寫或用個any來帶過
EN iT邦好手 1 級 ‧ 2022-12-08 11:40:12 檢舉
我覺得 API 的請求以及回覆內容可以特別檢查一下,我自己都是在這些地方踩坑...
Skywalker iT邦新手 5 級 ‧ 2022-12-08 22:25:27 檢舉
謝謝兩位大大,看了兩位的建議,我現在正在努力嘗試改寫內容,我覺得有點像froce大大說的,能寫得出來的就盡量寫,如果不行,也沒辦法,但自己寫的部分、能掌握的,就把型別寫上,讓別人也能讀得懂我寫的代碼。
然後API請求及回來的資料型別我也會注意,謝謝~
Skywalker iT邦新手 5 級 ‧ 2022-12-09 01:19:13 檢舉
最後我自己修正錯誤了,沒有報錯,且能夠成功的展示數據給用戶。
有興趣的人可以至下方連結查看結果。
https://ithelp.ithome.com.tw/articles/10310280
froce iT邦大師 1 級 ‧ 2022-12-09 16:25:21 檢舉
恭喜,但如果人家給的api文件齊全,建議還是要寫型別。
JSON用class是可以表示的,只是得一層一層拆解下去。
Skywalker iT邦新手 5 級 ‧ 2022-12-09 22:00:17 檢舉
了解,我會再嘗試看看,謝謝froce大大,繼續努力:)
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友回答

立即登入回答