TypeScript 和 ReasonML 都聲稱自己爲 Web 開發人員提供了可編譯爲 JavaScript 的靜態類型語言,那麼它們之間的差別是什麼?ReasonML 能帶來 TypeScript 想要做到的一切(甚至更多),但是前者沒有那些 JavaScript 怪癖。這樣的話,你是否應該試試它呢?
TypeScript 是 JavaScript 的超集,這既是它的最佳特性,也是它最大的缺陷。雖說與 JavaScript 的相似性給了人熟悉的感覺,但這意味着我們所喜愛和反感的所有 JavaScript 怪癖都在 TypeScript 裏重現了。TS 只不過是在 JavaScript 之上添加了類型,然後就差不多完事了。
ReasonML 提供的是一種完全不同但讓人感覺很熟悉的語言。這是否意味着JavaScript /TypeScript 開發人員會很難學習這個新語言?我們就來看看吧。
聲明一個變量
讓我們從變量聲明開始:
ReasonML
let a = "Hi";
TypeScript
const a = "Hi"
在 ReasonML 中,我們使用 let 關鍵字聲明一個變量。沒有 const,默認情況下 let 是不可變的。
在這種情況下,兩種語言都可以推斷出 a 的類型。
函數
TypeScript
let sum = (a: number, b: number) => a + b
ReasonML
let sum = (a,b) => a + b;
儘管我沒有手動編寫任何類型,但這個函數的參數還是類型化了。爲什麼我們用不着在 ReasonML 中編寫類型呢?因爲強大的類型系統可以進行出色的類型推斷。這意味着編譯器可以在不需要你幫助的情況下推斷出類型。ReasonML 中的 (+) 運算符僅適用於整數——a 和 b 只能是這種類型,因此我們不必編寫它們。但如果需要,你隨時都可以編寫類型:
ReasonML
let sum = (a: int, b: int) => a + b;
接口,記錄
TypeScript
interface Product {
name: string
id: number
}
ReasonML 中最接近接口(Interface)的是記錄(Record,https://reasonml.github.io/docs/en/record)。
ReasonML
type product = {
name: string,
id: int,
};
記錄就像 TypeScript 對象一樣,但前者是不可變的,固定的,並且類型更嚴格。下面我們在某些函數中使用定義的結構:
ReasonML
let formatName = product => "Name: "++product.name;
TypeScript
const formatName = (product: Product) => "Name: " + product.name
同樣,我們不需要註釋類型!在這個函數中我們有一個參數 product,其屬性 name 爲字符串類型。ReasonML 編譯器可以根據使用情況猜測該變量的類型。因爲只有 product 這個類型具有字符串類型的 name 屬性,編譯器會自動推斷出它的類型。
更新記錄
let updateName = (product, name) => { ...product, name };
const updateName = (product: Product, name: string) => ({ ...product, name })
ReasonML 支持展開運算符,並像 TypeScript 一樣對名稱和值做類型雙關。
看看 ReasonML 生成了什麼樣的 JavaScript 吧,這也很有趣:
function updateName(product, name) {
return [name, product[1]]
}
ReasonML 中的記錄表示爲數組。如果人類可以像編譯器一樣記住每種類型的每個屬性的索引,則生成的代碼看起來就會像人類編寫的那樣。
Reducer 示例
我認爲這就是 ReasonML 真正閃耀的地方。我們來比較一下相同的 Reducer 實現:
在 TypeScript 中(遵循這份指南:https://redux.js.org/recipes/usage-with-typescript)
interface State {
movies: string[]
}
const defaultState: State = {
movies: [],
}
export const ADD_MOVIE = "ADD_MOVIE"
export const REMOVE_MOVIE = "REMOVE_MOVIE"
export const RESET = "RESET"
interface AddMovieAction {
type: typeof ADD_MOVIE
payload: string
}
interface RemoveMovieAction {
type: typeof REMOVE_MOVIE
payload: string
}
interface ResetAction {
type: typeof RESET
}
type ActionTypes = AddMovieAction | RemoveMovieAction | ResetAction
export function addMovie(movie: string): ActionTypes {
return {
type: ADD_MOVIE,
payload: movie,
}
}
export function removeMovie(movie: string): ActionTypes {
return {
type: REMOVE_MOVIE,
payload: movie,
}
}
export function reset(): ActionTypes {
return {
type: RESET,
}
}
const reducer = (state: State, action: Action) => {
switch (action.type) {
case ADD_MOVIE:
return { movies: [movie, ...state.movies] }
case REMOVE_MOVIE:
return { movies: state.movie.filter(m => m !== movie) }
case RESET:
return defaultState
default:
return state
}
}
沒什麼特別的,我們聲明瞭狀態界面、默認狀態、動作、動作創建者以及最後的 Reducer。
在 ReasonML 中也是一樣:
type state = {
movies: list(string)
};
type action =
| AddMovie(string)
| RemoveMovie(string)
| Reset
let defaultState = { movies: [] }
let reducer = (state) => fun
| AddMovie(movie) => { movies: [movie, ...state.movies] }
| RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }
| Reset => defaultState;
/* No need for additional functions! */
let someAction = AddMovie("The End of Evangelion")
對,就這些。
讓我們看看這裏發生了什麼。
首先,有一個狀態類型聲明。
之後是動作 Variant(https://blog.dubenko.dev/typescript-vs-reason/#https://reasonml.github.io/docs/en/variant)類型:
type action =
| AddMovie(string)
| RemoveMovie(string)
| Reset
這意味着具有類型 action 的任何變量都可以具有以下值之一:Reset、帶有一些字符串值的 AddMovie 和帶有一些字符串值的 RemoveMovie。
ReasonML 中的 Variant 是一項非常強大的功能,可讓我們以非常簡潔的方式定義可以包含值 A 或 B 的類型。是的,TypeScript 有聯合類型,但它沒有深入集成到語言中,因爲 TypeScript 的類型是給 JavaScript 打的補丁;而 Variant 是 ReasonML 語言的重要組成部分,並且與模式匹配等其他語言功能緊密相連。
說到模式匹配,我們來看一下 Reducer。
let reducer = (state) => fun
| AddMovie(movie) => { movies: [movie, ...state.movies] }
| RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }
| Reset => defaultState;
我們在這裏看到的是一個函數,該函數接受狀態作爲第一個參數,然後將第二個參數與可能的值匹配。
我們也可以這樣寫這個函數:
let reducer = (state, action) => {
switch(action) {
| AddMovie(movie) => { movies: [movie, ...state.movies] }
| RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }
| Reset => defaultState;
}
}
由於匹配參數是 ReasonML 中的常見模式,因此這類函數以較短的格式編寫,如前面的代碼片段中所示。{ movies: [movie, …state.movies] }這部分看起來與 TypeScript 中的一樣,但是這裏發生的事情並不相同!在 ReasonML 中,[1,2,3] 不是數組,而是不可變的列表。可以想象它是在語言本身內置的 Immutable.js。在這一部分中我們利用了一個事實,即 Append 操作在 ReasonML 列表中的時間是恆定的!如果你之前用的是 JavaScript 或 TypeScript,你可能會隨手寫下這種代碼,無需太多顧慮,並且可以免費獲得性能提升。
現在,讓我們看看向 Reducer 添加新動作的操作。在 TypeScript 中是怎麼做的呢?首先你要在類型定義中添加一個新動作,然後以動作創建者的形式編寫一些樣板,當然不要忘了在 Reducer 中實際處理這種情況,一般這一步都容易被忽略。
在 ReasonML 中,第一步是完全相同的,但是後面就都不一樣了。在將新動作添加到類型定義後單擊保存時,編譯器會帶你在代碼庫中慢慢挪動,處理對應的情況。
你會看到這樣的警告:
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
Sort
這是很好的開發體驗。它會指出你要處理新動作的確切位置,並且還會準確告訴你缺少哪種情況。
Null、undefined vs Option
在 TypeScript 中我們需要承受 JavaScript 留下來的負擔,也就是用 null 和 undefined 來表示幾乎一模一樣的事物——毫無意義。
在 ReasonML 中沒有這種東西,只有 Option 類型。
ReasonML
type option('a) =
| Some('a)
| None;
這是一個大家很熟悉的 Variant 類型。但是它也有一個類型參數’a。這很像其他語言中的泛型,比如說 Option。
來對比更多的代碼看看:
interface User {
phone?: number
}
interface Form {
user?: User
}
function getPhone(form: Form): number | undefined {
if (form.user === undefined) {
return undefined
}
if (form.user.phone === undefined) {
return undefined
}
return form.user.phone
}
訪問可以爲空的屬性是最簡單的情況之一,閉着眼也能寫出來。在 TypeScript 中,我們可以啓用嚴格的 null 檢查然後手動檢查 undefined 的值來糾正它。
open Belt.Option;
type user = {
phone: option(int)
};
type form = {
user: option(user)
};
let getPhone = form =>
form.user->flatMap(u => u.phone);
在 ReasonML 中,我們可以使用內置的 option 類型和 Belt 標準庫中的輔助函數,然後就能以標準化方式處理可能爲空的值。
帶標籤的參數
我覺得所有人都會認爲帶標籤的參數功能真是太棒了。可能每個人都必須在某個時候查詢函數參數的順序或含義。不幸的是,TypeScript 中沒有帶標籤的參數。
TypeScript
function makeShadow(x: number, y: number, spread: number, color: string) {
return 0
}
const shadow = makeShadow(10, 10, 5, "black") /* meh */
在 ReasonML 中,你在參數名稱前加一個~ 字符,然後它就被標記了。
ReasonML
let makeShadow = (~x: int, ~y: int, ~spread: int, ~color: string) => {
0;
}
let shadow = makeShadow(~spread=5, ~x=10, ~y=10, ~color="black")
是的,你可以在 TypeScript 中嘗試使用對象作爲參數來模擬這種做法,但是隨後你需要在每個函數調用時分配一個對象 :/
TypeScript
function makeShadow(args: {
x: number
y: number
spread: number
color: number
}) {
return 0
}
const shadow = makeShadow({ x: 10, y: 10, spread: 5, color: "black" })
模塊系統
在 TypeScript 中,我們顯式導出和導入文件之間的所有內容。
Hello.ts
export const test = "Hello"
import { test } from "./Hello.ts"
console.log(test)
在 ReasonML 中,每個文件都是一個帶有該文件名的模塊。
Hello.re
let test = "Hello";
Js.log(Hello.test);
你可以 open 模塊,使內容可用而無需模塊名稱前綴:
open Hello;
Js.log(test);
編譯速度
爲了對比編譯速度,我們來編譯 TodoMVC,因爲其實現和項目大小都是容易對比的。我們正在測試的是將代碼轉換爲 JavaScript 所花費的時間。沒有打包,壓縮等操作。[注 1]
TypeScript + React.js
$ time tsc -p js
tsc -p js 6.18s user 0.24s system 115% cpu 5.572 total
6.18 秒
ReasonML + ReasonReact
$ bsb -clean-world
$ time bsb -make-world
[18/18] Building src/ReactDOMRe.mlast.d
[9/9] Building src/ReactDOMRe.cmj
[6/6] Building src/Fetch.mlast.d
[3/3] Building src/bs_fetch.cmj
[12/12] Building src/Json_encode.mlast.d
[6/6] Building src/Json.cmj
[7/7] Building src/todomvc/App.mlast.d
[3/3] Building src/todomvc/App-ReasonReactExample.cmj
bsb -make-world 0.96s user 0.73s system 161% cpu 1.049 total
0.96 秒
現在這還包括編譯 ReasonML 依賴項的時間,我們還可以測試只編譯項目文件的時間:
ReasonML + ReasonReact, only src/
$ bsb -clean
$ time bsb -make-world
ninja: no work to do.
ninja: no work to do.
ninja: no work to do.
[7/7] Building src/todomvc/App.mlast.d
[3/3] Building src/todomvc/App-ReasonReactExample.cmj
bsb -make-world 0.33s user 0.27s system 117% cpu 0.512 total
0.33 秒
竟然有這麼快!
BuckleScript 將安裝時、構建時和運行時的性能視爲一項重要功能
這是從 BuckleScript 文檔中引用的。BuckleScript 是將 ReasonML 轉換爲 JavaScript 的工具。
目前TypeScript 勝過 ReasonML 的地方
關於 ReasonML 的內容並不是都那麼美好。它是一種相當新的語言……嗯,實際上它不是基於 OCaml 的,後者已經相當老了;但重點在於互聯網上的資源仍然不是特別多。與 ReasonML 相比,用谷歌搜索 TypeScript 的問題更容易獲得答案。
DefinitelyTyped 類型實在太多了,ReasonML 想要追上來還有很長的路要走。
結語
對於前端開發人員來說,ReasonML 語法應該讓人覺得很熟悉,這意味着學習曲線的起點並不那麼陡峭(但仍然比 TypeScript 要陡)。ReasonML 充分利用了其他語言和工具(例如 Immutable.js 和 eslint)中最好的東西,並將其帶入了語言級別。它並沒有試着成爲一種完全純粹的編程語言,你需要的話可以隨時退回到突變和命令式編程。它非常快,它的速度是提升開發體驗的重點所在。ReasonML 能帶來 TypeScript 想要做到的一切(甚至更多),但是前者沒有那些 JavaScript 怪癖。你應該試試它!
註釋
[1] 編譯時間測試平臺是一部 MacBook Pro 2015,處理器是 Intel Core i5-5287U @2.90GHZ
TypeScript + React.js 源碼(https://github.com/tastejs/todomvc/tree/master/examples/typescript-react)
ReasonML + ReasonReact 源碼(https://github.com/reasonml-community/reason-react-example/tree/master/src/todomvc)
原文鏈接: