时间: 2020-09-03 00:08:26 人气: 2274 评论: 0
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 中的记录表示为数组。如果人类可以像编译器一样记住每种类型的每个属性的索引,则生成的代码看起来就会像人类编写的那样。
我认为这就是 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
这是很好的开发体验。它会指出你要处理新动作的确切位置,并且还会准确告诉你缺少哪种情况。
在 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 技术沙龙 教程文章 热点综合