前言
學完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",只好先在文章標題加上已解決。