---
# System prepended metadata

title: 'TypeScript  菜雞:chicken:小分享'
tags: [' TypeScript', react]

---

---

tags: react, TypeScript
---

# TypeScript  菜雞:chicken:小分享

在閱讀以下關於使用TypeScript的經驗時，或許你會需要先初步了解TypeScript為何物，官方網站: [TypeScript](https://www.typescriptlang.org/docs/handbook/basic-types.html)

而我偏好使用type勝於interface，因此以下大多使用type

<!-- 1. 簡介
2. 環境設置 / 套件
3. with React(create react app --typescript)
4. 文件架構
5. js -> ts 使用後心得 / 問題(約二個月)
6. TypeScript work flow -->

---

## 1. 簡介

### TypeScript: JavaScript的一個嚴格超集，簡單來講就是在JavaScript的基礎上，加上type, interface, class...等的型別定義．
因此特色是JavaScript所沒有的靜態型別，透過型別來解決JavaScript的弱型別問題，在web大型專案的好處會因為其TypeScript的強型別而更有優勢．

---

透過簡單的function範例，馬上來看兩者的差別

在==JavaScript==中，如果要加總數字，你即便給的是字串也不會有問題，function照樣把字串加總起來，但就可能不會是你預期的答案

而==TypeScript==中，透過在參數定義型別的方式，強迫參數一定要是number，則typeScript在compile的時候就會檢查使用者給的變數，而在compile後，或是vsCode你有用eslint/tslint，就會出現error提醒你的變數用錯了！

---

簡單程式碼範例比較: 
```typescript=
//javaScript
function sumNumbers(a, b) {
  return a + b;
}
console.log(sumNumbers('1' + '2')) //"12" 沒問題，但是變成string相加!


//typeScript
function sumNumbers(a: number, b: number) {
  return a + b;
}

//error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
console.log(sumNumbers('1' + '2')) 
//ok
console.log(sumNumbers(1 + 2)) //3
```

---

## 2. 從頭開始的基本環境設置(from scractch with react)
以下皆為基礎設定，額外的設定需個別查看module的文檔

範例檔案結構(依照專案設定會有所不同，以下僅為基礎示範):
```
|-tsconfig.json
|-webpack.config.js
|-src
  |-index.tsx
  |-index.html
  |-components
    |-...
|-package.json
|-yarn.lock
```

---

module安裝: 

```typescript=
//core modules ---
yarn add react react-dom typescript

//dev modules ---

//webpack
yarn add --dev webpack webpack-cli webpack-dev-server

//@types
yarn add --dev @types/react @types/react-dom

//loaders(也可用其他typescript loader), plugins
yarn add --dev awesome-typescript-loader html-webpack-plugin
```
---

webpack configs:

```typescript=
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.tsx',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        //使用awesome-typescript-loader，不用babel...等
        use: 'awesome-typescript-loader', 
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [ '.tsx', '.ts', '.js' ]
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}
```

---

tsconfig.json:

```json=
{
  "compilerOptions": {
    "outDir": "./dist/", 
    "sourceMap": true, 
    "allowJs": true,
    "module": "esnext",
    "moduleResolution": "node", //because react uses csstype module
    "allowSyntheticDefaultImports": true, 
    "target": "es5",
    "jsx": "react"
  }
}
```
==allowSyntheticDefaultImports==  true/false差別: 

```typescript=
//在default import 會有差別

//true
import React from 'react'

//false
import * as React from 'react'
```

---

index.tsx
```typescript=
import React from 'react'
import { render } from 'react-dom'

const Component = () => {
  return (
    <div>
      {'HI :)'}
    </div>
  )
}
export default Component

render(<Component />, document.getElementById('root'))
```

---

package.json

```json=
{
  //...,
  "scripts": {
    "start": "webpack-dev-server --open --watch",
    "build": "webpack"
  }
}
```
---

啟動專案(localhost)
```
npm start
```

---

## 3. VScode相關設置

直接用eslint搭配TypeScipt

在vsCode的==User/settings.json==增加以下設定
```json=
"eslint.validate": [
  "javascript",
  "javascriptreact",
  // ts
  {
      "language": "typescript",
      "autoFix": true
  },
  // tsx
  {
      "language": "typescriptreact",
      "autoFix": true
  },
]
```

---

### 安裝好了node modules卻無法import?(@types)

明明module都裝好了，可是import時找不到？

這時候就是 ==@types==的問題了，coding時在vsCode中從modules引入的東西，會提示你為哪種型別，function該怎麼用，這個就是該module的@types檔(通常是index.d.ts)提供的型別了，而在.ts/.tsx中import的modules，都是從@types而來，然而有些modules會沒有撰寫@types的型別檔，因此就會辨識為無此module並且無法正確import．

---

那麼會有以下兩種情況: 有/沒有 @types 的module

有@types可以裝: 

```typescript=
//以 react 做舉例

yarn add @types/react //這樣在.ts/.tsx才可以正確引入react
```
---

沒有@types可以裝:

1. 自己寫types檔

以someLibrary這個module做舉例
像是以下function引入時失敗，表示someLibrary沒有type檔
```typescript=
// import failed! module not found...
import { sum } from 'someLibrary'
```
---

2. 自訂type檔

以新建my-types為自訂types檔的資料夾做為舉例
資料結構
```
...
|- src
|- my-types
  |- someLibrary
    |- index.d.ts
```

則按照其module的function去撰寫types

```typescript=
//my-tyes/someLibrary/pesindex.d.ts

decalare module 'someLibrary' { //import failed的那個module的名稱
  export type sum = (a: number, b: number) => number //根據該function
}
```
---

3. 設定tsconfig.json中的typeRoots 

```typescript=
//tsconfig.json
{
  "compilerOptions": {
    ...,
    "typeRoots": [
      "./my-types" //這邊加上自訂types檔的路徑，typescript就會使用這些types
    ]
  }
}
```

---



## 4. 搭配React使用(含React專案之相關設定)

### 使用create react app馬上設定好基本環境(省去webpack, jest...等的設定)


```typescript=
npx create-react-app my-app --typescript
# or
yarn create react-app my-app --typescript
```
### 其他常用module的設定

storybook: 修改config & webpack.config.js


```typescript=
// .storybook/config.ts
...
const req = require.context("../src", true, /.stories.tsx$/); //改為.tsx
...


// 需安裝awesome-typescript-loader的module
// .storybook/webpack.config.js

module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    use: [
      {
        loader: require.resolve('awesome-typescript-loader'),
      },
      // Optional
      // {
      //   loader: require.resolve('react-docgen-typescript-loader'),
      // },
    ],
  });
  config.resolve.extensions.push('.ts', '.tsx');
  return config;
};
```

---

jest: setupTests.ts 及 CLI指令

```typescript=
// src/setupTests.ts

//如果有用 enzyme
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

//jest相關設定
module.exports = {
  collectCoverageFrom: [
    "src/**/*.{ts,tsx}",
    "!**/stories/**",
    "!**/node_modules/**"
  ],
};
```

```
//test coverage
//需要加上 --watchAll=false 

react-scripts test --watchAll=false --coverage"
```

---

## 5. React with TypeScript 工作流


==主要會有幾種狀況:==
>1. 先只有畫面(wireframe/彩稿/user story...等) 
>2. 先只有API 
>3. API和畫面皆有

---

1. 先只有畫面(wireframe/彩稿)：components based

透過先定義component自身的type開始:

```typescript=
type Props = {
  width: number
  height: number
}

const Box = ({
  width, height
}: Props) => <div style={{width, height}} />

<Box width={20} height={20} /> //OK
<Box width={20} /> //error: type不符合...

//如此一來，即可清楚定義一個components的props，
//透過約束該props值來保證components的使用正確

```

---

2. 先只有API ：API based

先從定義API的type開始
```typescript=
interface API {
  id: string
  name: string
  value: number
}

const Box = ({
  name, value
}: API) => <div style={{width: value * 10}}>{name}</div>

//直接透過API定義components的props

```

---

3. API和畫面皆有

先透過components/API去觀察其資料面貌，以小至大來定義types，找出可以共用的types，以減少維護複雜度 :+1: 

```typescript=
//components
import { User } from '...'

const UserBox = (props: User) => (
  <div>{props.user}: {props.status}</div>
)


//API

//原本API type
type Article = {
  user: string
  status: string
  content: string
  likes: number
}

//拆解共用 >>
export type User = {
  user: string
  status: string
}

type Article = User & {
  content: string
  likes: number
}
```

---


4. custom types: 當types被共用或是常用到時，歸納types用起來會更方便且更容易修改 

my-types/...
```typescript=
//常用的ID type
export type ID = number
```

components / functions...
```typescript=

function filterMinId = (id: ID, min: number) => {
  return id >= min //透過約束ID來做型別判斷
}

const BoxWithId = (props: {
  id: ID
}) => <div style={{width: id * 10}}>{id}</div>

```

---

## 6. TypeScript使用後與JavaScript比較，以及遷移的過程

* ### types的靈活度

前面提到共用types另外獨立出來，即是和types的靈活度有關，一旦type定義的型別越多，則components/functions更難共用其types，且可能會更難做測試(耦合程度可能比較高)，由以下兩個例子去比較:

---

#### 例子Ａ：(不做type處理)

如果是依賴上游(意即源頭，像是API或是其他的結果...，就像以下例子)的types，則上游types一旦更改了，那麼就需要一個個手動更改! 
```typescript=
type API = {
  id: string
  name: string
  serial: number
  gender: 1 | 2
  content: string
}

//以下為依賴API的components/functions

type Props = {
  name: string
  content: string
}
const User = (props: Props) => <Box {...props} />

function getGender(gender: 1 | 2) {
  return gender === 1 ? 'man' : 'woman'
}
```

---

#### 例子Ｂ：(type已經整理/事先規劃好type)

如果將會共用或常用的types事先分離(專案越早期越好)，之後就會省下修改和檢查的時間，且更好維護和測試!
```typescript=
//共用
type User = {
  name: string
  content: string
}
type Gender = 1 | 2
type GenderStr = 'man' | 'woman'

//組合
type API = User & {
  id: string
  serial: number
  gender: Gender
}

//以下為依賴API的components/functions

type Props = User
const User = (props: Props) => <Box {...props} />

const user_mockData: User = { //更好做mock for tests
  name: '',
  content: ''
}

function getGender(gender:Gender): GenderStr {
  return gender === 1 ? 'man' : 'woman'
}
```

---

* ### 測試驅動開發 & 組件驅動開發: 
TDD(Test Driven Development) & CDD(Components Driven Development)
前面提到關於優化或是整理types，都是以兩者的原則為基礎！
以下提到的正例和反例，皆是根據範例的比較而來，僅為較遵守以及較為不遵守原則，畢竟沒有以量化的標準去區分

---

TDD: 以通過測試及撰寫測試為前提，一旦有程式可以進行測試，則以測試優先執行，測試完成後再接著下一步，以這樣的方式來開發整個專案

---

CDD: 以個別組件為基礎單元，整個頁面的UI就會是以個別單元組件去組合(react 通常會配合 storybook等module去配合呈現個別組件)，專案的畫面即是以此呈現

---

TDD 與非 TDD簡單舉例(僅為個人習慣寫法的舉例):


* 非TDD習慣寫法: function就會有==3x2x2=12種==的情況去做測試
* 而採用TDD習慣寫法: 將function複雜度降低，則只有==3+2+2=7==種情況去做測試
```typescript=
//非TDD的習慣寫法
function calNumbers(a: number, b: number, c?: number=1) {
  const sum = a > b ? a + b : a < b ? a - b : 0
  const sum2 = sum > a ? sum + a : sum + b
  return sum2 > c ? sum2 * c : sum2
}

//TDD習慣寫法，如果個別function有更改，則只要改其function即可
const getSum = (a: number, b: number) => 
  a > b ? (a + b) : (a < b ? a - b : 0)
  
const getSum2 = (sum: number, a: number, b: number) => 
  sum > a ? sum + a : sum + b

const calNumbers = (a: number, b: number, c?: number) => {
  const sum = getSum(a, b)
  const sum2 = getSum2(sum, a, b)
  return sum2 > c ? sum2 * c : sum2
}
```
---

CDD 與CDD簡單舉例(僅為個人習慣寫法的舉例):
以一個簡單的頁面component做範例

* 非CDD: 一旦Page component的header或是article需要更改或是根據情境可以替換，那麼就需要找到article相關的code並加以更改，維護上以及在測試都會比較不方便
* CDD: 將Page component的header或是article獨立拆分成個別的components，那麼需要更改該components時，只需要個別處理，且更改的部分(像是在storybook或其他呈現UI的工具)就會一目瞭然看出更改前後的差別!

```typescript=
//not CDD
const Page = () => {
  return (
    <div>
      <header>
        <div>{'I am home page!'}</div>
      </header>
      <article>
        <h2>{'some title...'}</h2>
        <p>{'some contents...'}</p>
      </article>
      <footer>
        <ul>
          <li>{'about us...'}</li>
          <li>{'contact us...'}</li>
        </ul>
      </footer>
    </div>
  )
}

//CDD
const Header = () => {
  return (
    <header>
      <div>{'I am home page!'}</div>
    </header>
  )
}
const Article = () => {
  return (
    <article>
      <h2>{'some title...'}</h2>
      <p>{'some contents...'}</p>
    </article>
  )
}
const Footer = () => {
  return (
    <footer>
      <ul>
        <li>{'about us...'}</li>
        <li>{'contact us...'}</li>
      </ul>
    </footer>
  )
}
const Page = () => {
  return (
    <div>
      <Header />
      <Article />
      <Footer />
    </div>
  )
}
```

---

* ### 舊專案的 .js .jx -> .ts .tsx 遷移


js與ts最大差別在於types，因此原本寫的components/functions/class...都需要額外加上型別，雖然主要方法就如同前面所述的工作流的幾種方法(API, components, API & components)，

但是其中最大的差別就是在於撰寫types及規劃types，這是JavaScript所沒有的，而在定義型別的時候，會發現某些看起來很完美的程式，一旦要嚴格定義型別(除了any)，就會因此而更改或重構部分的程式碼，可能會有以下幾種情況(至少是我遇到的)：

---
#### 嚴格定義型別，有時會需要轉換格式(像是Number(), String(), ...等的方法)
```typescript=
//.js: 原本寫法, 都是假設資料皆為string
const getMessages = (id, content) => (
  id + content
)

type ID = string | number
//結果資料其實會有number型別，那麼就要修改加上格式轉換的部分，
//以避免其他consume此function時出現問題!
const getMessages = (id: ID, content: string | number): string => (
  id.toString() + content.toString()
)
```
---

#### 除了上述修改的部分，有些型別的定義特別嚴格，需要透過額外定義的方式來限制其型別
```typescript=
type Position = [number, number]
const positionA = [0, 0];

//typeof positionA: number[] 並不相等於 Position: [number, number]
```

---

#### 上述提到的自訂types檔，在專案具有一定的規模時，建議以獨立資料夾管理其常用types
(各自types沒有共用的話，放在各自檔案中就好)

---
1. 建好檔案
```
|- src
|- my-types
  |- allCommonTypes
    |- index.d.ts
  |- pageA
    |- index.d.ts
  |- pageB
    |- index.d.ts
  ...
```

---


2. 設定tsconfig.json中的typeRoots(讓typescript知道你的自訂types)

```typescript=
//tsconfig.json
{
  "compilerOptions": {
    ...,
    "typeRoots": [
      "./my-types" //這邊加上自訂types檔的路徑，typescript就會使用這些types
    ]
  }
}
```
---
3. 使用types
```typescript=
import { ID } from 'allCommonTypes' //共用的
 
type Area = [number, number] //自己用的
...
```

---

* ### generics 泛型: function for types

可以帶入參數的types，就是generics 泛型，適合用在某些types會根據情境改變其型別
像是以下範例
```typescript=
//generics type
type CommonData<Data> = { //generics的參數，使用<>包起來
  id: string
  data: Data
}

//使用1
type UserData = CommonData<{name: string}>
//等同於
type UserData = {
  id: string
  data: {
    name: string
  }
}

//使用2
type ImageData = CommonData<{
  src: string
  created_date: string
}>
//等同於
type ImageData = {
  id: string
  data: {
    src: string
    created_date: string
  }
}

```

---

除了一般的types，其實generics更多是用在function，讓function的型別更加彈性更好用！
```typescript=
//沒用generics
type A = (x: string) => string
type B = (x: number) => number

//用了之後
type GetData<DataType> = (x: DataType) => DataType

type A = GetData<string>
type B = GetData<number>
```








