Blog

/

Frontend

Next.js - TypeScript vs JavaScript

Next.js JS TypeScript

Posted at

2024年10月20日

Posted on

Frontend

不少人說用 TypeScript 寫起來很慢很麻煩,包含我現職的公司也是用 JavaScript。JavaScript 在小項目沒問題,但一旦項目複雜起來,TypeScript 就會節省你大量時間。特別在 Next.js 裡,路由、API、資料流動,這些地方很容易出運行時 bug,TypeScript 能幫你提前抓出來。

TypeScript vs JavaScript 快速對比

特性

JavaScript

TypeScript

優勢

編譯檢查

完整的型別檢查

TypeScript

API 返回值驗證

運行時拿到錯誤欄位

編譯時提示不存在的欄位

TypeScript

開發速度(短期)

慢 20-30%

JavaScript

代碼自動完成

需要自己記住欄位名

IDE 自動列出可用欄位

TypeScript

code 協作

同事間理解不易

溝通成本低

TypeScript

適合項目規模

一個人、簡單網站

三人以上、複雜應用

看場景

路由參數:寫一寫爆炸 vs 提前警告

假設你在做一個產品詳情頁。JavaScript 版本。

// app/products/[id]/page.js
export default function Product({ params }) {
  return (
    <div>
      <h1>Product {params.id}</h1>
      <p>Category: {params.category}</p>
    </div>
  )
}

看起來沒問題吧?但這個頁面的路由其實是 /products/[id],根本沒有 category 這個參數。代碼在開發時能跑,但 params.category 會是 undefined。到了測試或生產環境,頁面顯示「Category: undefined」,用戶投訴。

TypeScript 版本。

// app/products/[id]/page.tsx
type Params = {
  id: string
}

type Props = {
  params: Params
}

export default function Product({ params }: Props) {
  return (
    <div>
      <h1>Product {params.id}</h1>
      <p>Category: {params.category}</p>
    </div>
  )
}

這邊在寫到 params.category 時,編譯器馬上就會 error 了 "Property 'category' does not exist on type 'Params' " 就可以馬上發現自己的錯誤,改成正確的參數或加上 category。JavaScript 中要到 run 時才能發現,但在 TypeScript 中會直接編譯不過。

資料獲取時機

現在你需要從資料庫拿數據,JavaScript 版本:

// app/api/products/[id]/route.js
export async function GET(request, { params }) {
  const product = await db.products.findById(params.id)
  
  return Response.json({
    id: product.id,
    name: product.name,
    price: product.price,
    tags: product.tags
  })
}

// app/products/[id]/page.js
export default async function Product({ params }) {
  const response = await fetch(`/api/products/${params.id}`)
  const product = await response.json()
  
  return (
    <div>
      <h1>{product.title}</h1>
      <p>${product.price}</p>
      {product.tags?.map(tag => <span key={tag}>{tag}</span>)}
    </div>
  )
}

看見問題沒?API 返回的欄位是 name,但前端代碼用的是 title。因為沒有型別檢查,這種錯誤編譯時完全看不出來。頁面上顯示空白,你得在瀏覽器 DevTools 裡 console.log 才能發現。

// lib/types/product.ts
export type Product = {
  id: string
  name: string
  price: number
  tags: string[]
}

// app/api/products/[id]/route.ts
import { Product } from '@/lib/types/product'

export async function GET(request: any, { params }: { params: { id: string } }): Promise<Response> {
  const product = await db.products.findById(params.id)
  
  return Response.json({
    id: product.id,
    name: product.name,
    price: product.price,
    tags: product.tags
  } as Product)
}

// app/products/[id]/page.tsx
import { Product } from '@/lib/types/product'

export default async function Product({ params }: { params: { id: string } }) {
  const response = await fetch(`/api/products/${params.id}`)
  const product: Product = await response.json()
  
  return (
    <div>
      <h1>{product.title}</h1>
      <p>${product.price}</p>
      {product.tags?.map(tag => <span key={tag}>{tag}</span>)}
    </div>
  )
}

現在 product.title 會馬上報錯。TypeScript 知道 Product type 沒有 title,只有 name。你在編譯時就發現錯誤,改成 product.name。

這是個小例子,但在大項目中,這類錯誤會出現上百次。JavaScript 則是每一次運行都有機會遇到驚喜。

重構過程的比較

假設你改了資料庫 schema,把 price 改成 priceInCents(以分為單位)。需要在所有讀取 price 的地方做轉換。

JavaScript 版本,你得手動搜索所有用 product.price 的地方。

// app/products/[id]/page.js
<p>${product.price / 100}</p>

// app/components/ProductCard.js
<span>${product.price / 100}<

你搜索漏掉一個地方,某個頁面顯示的價格就是錯的。用戶看到 1000 元的東西被當作 10 元賣,投訴來了。

TypeScript 版本。

// lib/types/product.ts
export type Product = {
  id: string
  name: string
  priceInCents: number  // 改名稱
  tags: string[]
}

// 編譯器會提示所有用到 product.price 的地方都出錯了
// app/products/[id]/page.tsx
<p>${product.price / 100}</p>  // 編譯錯誤:Property 'price' does not exist

// app/components/ProductCard.tsx
<span>${product.price / 100}<

重構變成了一個系統化的過程。編譯器會提示你遺漏的任何地方。

開發速度:點擊和搜索 vs 自動補全

寫 JavaScript 時,你要自己記住有哪些欄位。

const product = await fetchProduct(id)
console.log(product.) // 這時候你得猜 或者自己查資料庫 schema

寫 TypeScript 時,編輯器知道有哪些欄位。

const product: Product = await fetchProduct(id)
console.log(product.) // 編輯器自動列出 id, name, priceInCents, tags

你可以直接點選,或者按上下箭頭選擇。想查某個欄位的類型,Hover 一下就看到。

程式協作

在 JavaScript 專案中,新來的人很難理解代碼的意圖(現職公司也遇到這個狀況 :P)。這個 params 到底是什麼形狀? response 有哪些欄位?

export async function updateProduct(id, data) {
  // id 是 string 還是 number?
  // data 裡面要有哪些欄位?
  // 返回什麼?
  return db.products.update({ id }, data)
}

他得問開發者,或者看文件,或者讀相關的測試代碼。效率很低。

TypeScript 版本。

type UpdateProductInput = {
  name?: string
  priceInCents?: number
  tags?: string[]
}

type UpdateProductResult = {
  success: boolean
  product?: Product
  error?: string
}

export async function updateProduct(id: string, data: UpdateProductInput): Promise<UpdateProductResult> {
  return db.products.update({ id }, data)
}

新人看這段代碼立刻知道 id 必須是 string,data 有哪些可選欄位,返回什麼。不用問人,代碼本身就是文件。

工具支持的差異

Next.js 對 TypeScript 有很多特殊支持。比如路由型別推導。

JavaScript 中,你不知道 page component 該接收什麼 props。

// app/posts/[slug]/page.js
export default function Post(props) {
  // props 是什麼形狀?得自己查文件
}

TypeScript 中,Next.js 提供了型別助手。

// app/posts/[slug]/page.tsx
import type { PageProps } from 'next/app'

export default function Post({ params, searchParams }: PageProps<{ slug: string }, { sort?: string }>) {
  // params 和 searchParams 的形狀完全明確
}

Metadata 也是。JavaScript 中你得自己記住怎麼定義 metadata。TypeScript 中,IDE 會自動補全。

運行時性能以及成本對比

要澄清一點,編譯後的 TypeScript 和 JavaScript 版本是完全一樣的。TypeScript 只是開發時的工具,編譯後就消失了。所以運行時性能沒有區別,TypeScript 不會讓你的應用更快或更慢。那使用 TypeScript 的成本是什麼?初期寫代碼會慢 20-30%,因為要加型別,但這個投資在哪裡回本?

小項目,一個人寫,改 code 不頻繁。TypeScript 沒什麼優勢,JavaScript 夠了。

中等項目,三五個人,不時有 bug、重構,TypeScript 就開始值錢。重構時編譯器幫你檢查,新人上手快,節省的時間超過初期的投入。

大項目,團隊多,複雜度高。TypeScript 不是可選的,是必須的,Bug 和溝通成本會吃掉你所有時間。

Next.js 項目的特點是很容易變大。今天一個簡單的著陸頁,三個月後就變成了複雜的全棧應用,用 JavaScript 開始其實是埋坑。

我的建議

新專案如果超過一個人或有複雜的資料流動,用 TypeScript。如果只是簡單的靜態網站,JavaScript 也行。

如果在維護老項目,已經用 JavaScript 了,不一定非要遷移。但新功能可以用 TypeScript 寫。

最重要的是,不要因為「TypeScript 很流行」就用 TypeScript。用 TypeScript 的理由很簡單:你想在編譯時發現 bug,而不是生產環境。

下次來開始講渲染機制的細節。Server Components 會怎麼執行,為什麼有那些限制。