我来说一句反对typescript的话

世界上刚有typescript的时候,我就开始关注它了。我很明确,它是一门新语言,只是和javascript是近亲。在它的介绍文档中,我看到private关键字,甚是兴奋。但我开始去用它的时候,发现它的private的骗人的。除了private骗人外,它的所谓类型检查系统也有欺骗性。现在很多人已经叫嚣着,不懂typescript就是不会前端。说实话,除了对typescript不满意,我连对ES标准的#符也不满意。不是我挑剔,是有些东西人会天然不喜欢。

读完这个答案之后,我更加确信,原来自己的反感是有根源的,并非我有问题,而是typescript有问题。有什么问题呢?先来说一些旁的不喜欢点:

  • 繁琐的类型定义方式,明明是一门新语言,非要按照js的语法来写,但是又发明一些莫名其妙的新语法
  • 类型声明实在受不了,我知道使用 : 是受了其他语言的启发,但是真正好的类型声明很明显要放在变量前面
  • 对象属性的类型声明?呵呵,太恶心。: 和 as 能让人写作烦躁10倍。
  • any?哎。。。
  • 结构类型检查!WTF
  • 据说类型检查能减少bug,但是,你怎么能确保你编写的类型本身没有bug?
  • 降低效率,用在解决(编写)一个类型问题上的时间,可能够我写完2个需求
  • 使代码可读性降低,你需要在阅读代码过程中,跳到类型定义的部分去阅读,阅读完再跳回来来,一去一来,我是谁,在哪里?
  • 增加运行难度,据说deno支持直接运行ts,但是实际上,还是先翻译为js后执行

我们来看上面那篇回答中的一个经典案例:

interface A {
    x: number;
}

let a: A = { x: 3 }
let b: { x: number | string } = a;

b.x = "unsound";

let x: number = a.x;

a.x.toFixed(0);

恶心死你。你不是静态类型检查吗?给你查,查破了你能告诉我 bug 来自哪里?人生啊,不要相信所谓了“在准确和效率之间找平衡”,忽悠。别的类型系统,之所以成立,是因为别的语言需要先编译,后执行,有健全类型系统,类型声明不用慌,而且类型本身就是运行时的。从我的不成熟的想法来看,不在运行时的类型系统,都是扯虎皮。使用typescript嘛,和语句末尾使用使用;结束一样,技术不到位,有;也避免不了各种错,技术到家,没;照样优雅健壮。

我理想中的js变量类型声明:

int a = 1
bigint b = 299300002390809238n
float c = 2.2
bigfloat d = 4.394085943789534809830543l
string e = 'xxx'
Date date = new Date()
Promise p = new Promise(...)

// 多层结构的对象
object o = {
  string name: 'my name',
  int age: 10,
  // 用<>表达其内部结构
  array<[
    object<{
      string name,
      number price,
    }>
  ]> books: [
    // 纯值,当然,也可以在内部进行类型定义,类型系统自己推演
    {
      name: 'book name',
      price: 6.6,
    },
  ],
}

let x = 1 // 相当于 any
const y = 'ok' // 不可变
var z = null // 带变量提升的 let

如果一个变量想要发生类型转化。。。不可以的。

int a = 10
string b = a + ''

这样操作是唯一的途径吧。

但是实际上,在运行时,你怎么知道后端接口会返回给你什么?大部分情况下,null 值无法避免。

// 用 type 关键字声明和定义类型
type ResponseType = object<{
  int code,
  object data: {
    string name,
    number|null age, // 支持 null
    array<[
      // array 内部可以有多种结构,只要元素满足其中之一,就可以通过类型检查
      object<{
        string name,
        number price,
      }>,
      object<{
        string name,
        int pages,
      }>,
    ]> books,
  },
  ?string error, // ? 开头表示可能不存在,存在的情况下才遵循类型
}>

现在我要使用它:

fetch('/api').then(res => res.json()).then(string (ResponseType data) => {
  // 如果data的类型检查不通过,直接抛出 TypeError
})

看看函数:

function int a(int x, int y) {
  return x + y
}

object o = {
  int a(int x, int y) {
    return x + y
  },
}

class Some {
  string _name = 'my name'

  // 类型声明永远放在变量名前面
  static get string name() {
    return this._name
  }
}
// 直接对函数进行参数和返回值类型声明
// <参数类型列表: 返回值类型> 这种表达式借鉴于 python lambda
// 如果没有参数类型列表,如function<int>,表示没有参数,返回值为 int
function<int, int: int> a(x, y) {
  return x + y
}

再搞一个泛型耍耍。我在这篇文章中已经说过了,泛型,实际上就是函数

// 用 <> => 定义泛型,其中 book<a, b> 中的 book 是泛型的名字,<a, b> 是替代符,=> 后面是返回的形式化结果
type book<a, b> => object<{
  a name,
  b price,
}>

怎么用?这样子啊:

book<string, number> book = {
  name: 'xxx',
  price: 12,
}

看看泛型的演化:

// 定义一个泛型,泛型的结果是一个 object 描述,(而非一个类型)
type a<x, y> => {
  x a,
  y b,
}

type b<z> => a<z, z> // 直接运行 a<z, z> 将得到的结果作为 b<> 泛型结果

// b<string> 的结果,作为 object<> 的参数
object<b<string>> o = {
  a: 'xxx',
  b: 'yyy',
}

实际上,我不大推崇这种声明,因为通过这样的声明,我并不能马上看清楚在声明 book 这个变量时,它的结构,我更喜欢:

object<{
  string name,
  number price,
}> book

即使这个时候,我并不给 book 赋值,这样声明 book 这个变量我就知道它的内部结构是啥。虽然这样我可能需要写很多重复代码,但是相对来说,理解成本更低。

当然,在函数上,泛型确实还是有好处:

function int|string a(int|string x, int|string y) {
  return x + y
}

这种声明明显不好,因为你怎么知道 x=1, y='a' 这种情况不会发生。所以,这种情况,要使用泛型啊:

// 通过泛型,实现函数参数和返回值统一类型
type f<T> => function<T, T: T>
f<int|string> a(x, y) {
  return x + y
}

// 或者通过一个匿名的泛型来简化写作
function<int|string as T => T, T: T> a(x, y) {
  return x + y
}

来详细解释一下这个语法

function<...> // 函数类型,<> 中是对函数类型的描述,上面说过函数的描述格式为 params:result
<类型 as 泛型> // as 关键字表示,将“类型”传入“泛型”作为参数,运行泛型得到结果
T => T, T: T // 一个匿名的泛型,如果泛型有多个参数,可以表达为 a, b => a, b: a|b
int|string as T => T, T: T // 将 int|string 作为参数 T 运行泛型 T => T, T: T,当 T 为 int 时,结果为 int, int: int,当 T 为 string 时,结果为 string, string: string
function<int|string as T => T, T: T> // 运行结果是 function<int, int: int> 或者 function<string, string: string>

其中 | 操作符,会带来一些逻辑上的混淆,需要明确:

function<int|string, string|number as a, b => a, b: a|b>
// 相当于
function<(int, number) | (string, number) | (string, string) | (int, string) as a, b => a, b: a|b>
// 但是我可能只想要
function<(int, number) | (string, number) as a, b => a, b: a|b>

到底应该是将 | 至于多个参数列表之间,还是置于参数位置本身?很显然,置于参数位置本身,还是会产生歧义,所以要有置于参数列表之间的 | 语法,需要通过 () 分组的逻辑。

这一套东西,是在运行时做的,这样的定义,岂不是爽歪歪。当然,我依靠 tyshemo 还是有可能把这套东西给实现的。对了,有兴趣可以看下 C# 的类型系统。

已有2条评论
  1. bigsir 2020-04-22 16:11

    whh

  2. rxliuli 2020-04-18 14:11

    不敢苟同,ts 的认知吾辈经历了反复的几次变化之后,只能说:真香!