3.0 TypeScript 问题答疑及经验分享

虽然 Creator 很早就支持了 TypeScript,直到 3.0 我们才正式废弃了对 JavaScript 的支持。但随着这次 3.0 的全面升级,很多第一次使用 TypeScript 的开发者仍然遇到了不少阻碍。

为了让大家的升级更加平滑,本文将对这次调整做一次简要回顾,并且解答开发者在语言升级的过程中容易遇到的一些问题。

我们在 TS 上的铺垫

我们对 TypeScript 的支持由来已久,最早可追溯到 17 年 5 月发布的 v1.5.0,此后我们一直在听取 TS 开发者的反馈,逐步完善 TS 的支持。

18 年的时候,3D 项目组开始用 TS 重写整个 3D 引擎和编辑器,验证了 TS 处理超大规模项目时的能力,让我们对 TS 有了更大的信心和第一手的使用经验。

到了 19 年,我们在 Cocos Creator 3D 正式发布时,第一次正式废弃了对 JavaScript 的支持,而后持续听取开发者们对这一改变的反馈。

要采用哪一门语言做为引擎的开发语言,不是一个能轻易做出的决定,对开发者、对生态都影响巨大。
Cocos Creator 当初选择 JS 时,已经经历过一次饱受非议,这次如果不是有前面几年在 TS 上的铺垫,我们也没有勇气在 3.0 做出这个关键调整。

为什么要转向 TS

在 Cocos Creator 14 年刚立项之初,由于熟悉的语法,TypeScript 成为了我们入门 JavaScript 的良好桥梁。但由于当时的 TS 刚刚正式发布,生态仍未成熟,因此我们只能先使用最原生的 JavaScript。

经过了这么多年的发展,今天 TS 已经成为一门非常成熟的语言,有丰富的生态和群众基础,可以胜任任何类型的游戏开发需要!

从我这几年跟研发团队的接触来看,大多数团队都逃不过 TS 的真香定律,网上也有大量对 TS 的安利文章,那么我这边就不赘述 TS 的优势。我分享下从我们的角度,我们是怎么想的。

  • 我们始终相信,良好的编程语言是项目成功的基石。诚然,任何语言和框架都只是工具,优秀的程序员不论用什么工具都能把项目做好。不过良好的语言更能降低项目整体风险,让开发人员心情愉悦,提升协作效率,从长远来看更能降低整个团队的研发成本!

  • 我们始终相信,Cocos Creator 不仅仅是一个小游戏引擎,它必定要能支撑大型重度游戏的开发。JavaScript 或许能满足高手自己做一些小项目的需要,或许能快速做点小东西玩玩,但是达不到大型项目的工业标准。

  • 我们始终相信,JavaScript 做为弱类型语言,迟早会成为 Cocos Creator 项目的优化瓶颈,只有强类型语言才有机会彻底提升整个项目的性能表现。所以我们持续在探索 JavaScript 编译为原生语言的可能性,这离不开 TypeScript 的支持。今天已经有 AssemblyScript 这样成熟的项目支持将 TypeScript 方言编译为 WebAssembly,事实上同类项目还有很多。只有从生态、社区的角度统一大家的开发语言,将来我们才有机会为大家送上这份大礼。

  • 我们始终相信,语言的割裂会带来生态的割裂。今天在 Cocos Creator 社区已经拥有了一帮 TypeScript 的簇拥,很多优质的教程、插件、帖子都使用 TypeScript 发布。如果官方文档、官方案例都使用一门语言编写,将不利于另一门语言的学习。如果社区长期拥有两门语言,更不利于大家复用前人的工作成果。JavaScript 开发者无法适应 TypeScript 项目的维护,TypeScript 开发者在复用原有的 JavaScript 组件时也会遇到阻碍。


以下内容由我们的引擎质监局长收集自论坛反馈

常见的 TypeScript 认知误区

误区:Cocos Creator 仅支持 TypeScript,不支持 JavaScript

TypeScript 是 JavaScript 的超集并且 TypeScript 紧紧依赖 JavaScript。Cocos Creator 3.0 仍支持 TypeScript 和 JavaScript 并用。

然而,Cocos Creator 鼓励用户使用 TypeScript 以获得更好的开发体验,提高开发质量,因此在编辑器中 仅支持创建 TypeScript 脚本

如果你确定一定要使用 JavaScript,以其他方式(资源管理器、访达等)创建 JavaScript 文件仍然是允许的。

误区:import/export 是 TypeScript 专有的,我无法在 JavaScript 中使用 require

import/export 是 JavaScript 规范 ECMAScript 2015 引入的对模块支持的语法,因此 JavaScript 中是可以使用的,并不是 TypeScript 专有。

另一方面,require/exports/module 是 CommonJS 模块系统中的变量。它们不属于 JavaScript 标准。

尽管,在 3.0 中提供了对 CommonJS 模块的有限支持。

将 JavaScript 代码迁移为 TypeScript

上述有提到 JavaScript 可以直接使用。但只要了解 TypeScript 的用法,将 JavaScript 代码迁移为 TypeScript 也是一件轻松的事。

最简单的迁移:改后缀名

可以直接将 .js 文件重命名为 .ts 完成最简单的迁移——只要 JavaScript 在运行时没问题,那么如此改为 TypeScript 也一定是没问题的。

改名后,在 IDE 中会有一大堆报错,这是因为缺少类型信息。如果你暂时不想解决这些类型问题,大可以在整个文件的头部加一句注释:

// @ts-nocheck

来跳过对该文件的类型检查。

在代码中补充类型信息

对于比较简单的 JavaScript 代码,只要稍加补充一些类型信息就可以。例如:

function fn(a, b, c) {
    // ...
}

fn 指定参数类型就 OK:

function fn(a: string, b: number, c: boolean) {
    // ...
}

TypeScript 会自动根据函数体内的代码推断出函数的返回值。你当然也可以显式指定:

function fn(a: string, b: number, c: boolean): string {
    // ...
}

在代码外部声明类型信息

有些时候,JavaScript 代码不是我们自己写的,而是由第三方提供的库代码。这时候我们可以在不编辑它的情况下为它补充类型信息。

例如,有一个第三方的 JavaScript 文件 foo.js:

module.exports = function foo (a, b, c) {
    // ...
}

我们可以在同目录下创建一个同名但扩展名为 .d.tsfoo.d.ts

// 声明 foo 的签名即可,函数体不需要
function foo(a: string, b: number, c: boolean): string;

export default foo;

严格模式:“null” 问题

从 JavaScript 转入 TypeScript 的同学可能被一些“类型问题”所困扰。

看这样一个问题:

class C {
    material: Material = null;
}

这段代码在 IDE 中会报错,报错源头是属性 material 的声明。

有一种情况是,material 属性仅在初始化时是空值,但是后续任何时候访问都是有值的。例如,给该属性附加 @property 装饰器时,就可以在编辑器中编辑该字段,拖拖拉拉之后由 Creator 帮我们赋值该字段。

那我们如何向 TypeScript 传达这种信息呢?

我们来分析一下报错的原因,material: Materialmaterial 字段声明为 Material 类型,这个意思就是在任何时候拿到 material 它都是 Material 类型。

初始化式 = null 告诉它将 material 初始化为 空值 null,与上条说法违背。

这便是,TypeScript 的严格类型检查。它要求你将类型对上号。TypeScript 编辑器是默认开启该选项的,Creator 也不例外。

既然知道了出错原因,那么我们就可以有以下几种思路去解决。

  1. 正确描述它的类型为:既可能是 Material,也可能是 null
  2. 向 TypeScript 类型系统表达:我只是初始化为 null,后续使用时候其实都是 Material
  3. 关闭 TypeScript 对空值的检查。

可空类型

我们可以将 material 的类型描述为既可能是 Material,也可能是 null

material: Material | null = null;

这样初始化为 null 就顺理成章了。

然而,这样的缺陷是当你后续访问 material 时,TypeScript 要求你处理空值的情况。例如:

console.log(this.material.name);

TypeScript 类型系统会提示你:this.material 可能是空值 null,无法访问 null 的属性。

但是,只要 TypeScript 在此处知道它一定是不为空的,它就会收起这条错误:

if (this.material) {
    console.log(this.material.name);
}

因为你做了判断,当运行到 if 语句块内的时候,this.material 一定是不为空的,因此可以访问 name 属性。

表达式非空断言

每次使用的时候都要用 if 语句判断一下实在有些繁琐,况且 if 语句在运行时是会去执行的。

如果你一定能确保“在运行到这里的时候 this.material 一定非空”,那么我们可以用TypeScript 非空断言语法来表达:

console.log(this.material!.name);

感叹号 ! 称作 非空断言操作符,它断言前面的表达式是非空的。

注意此感叹号是 TypeScript 特有的,仅为类型目的;编译后,会直接移除。

我们还可在初始化时就作此断言:

material: Material = null!; // 相当于将 `null` 强制转换为了 `Material` 类型

还一个经常用于非空断言的地方是 Node.getComponent(),此方法返回的是可能为空的组件。如果能确保组件一定存在,则可以通过非空断言来避免 if 判断。

显式赋值断言

在标准的 JavaScript 语法里,是可以不给初始化式的,这样的字段将被初始化为 undefined

class C {
  @property(Material)
  material; // undefined
}

在 TypeScript 里也允许这么做,不过你得提示一下 TypeScript,以让它跳过这里的类型检查:

material!: Material;

属性名后的感叹号称为 显式赋值断言(Definite Assignment Assertion),它告诉 TypeScript 此字段在别处初始化。

OK,这里你将得到一个虽然声明为 Material,但是初始化为 undefined 的字段。

禁用空值检查

当然了,如果你是非常不喜欢 TypeScript 的类型系统,你更喜欢所有事靠自己确保,那么你可以关闭严格类型检测。在 <项目目录>/tsconfig.json 里,加上选项:

"extends": "./temp/tsconfig.cocos.json",
"compilerOptions": {
  "strictNullChecks": false // 关闭它
}

需要提醒的是,我们并不鼓励这种做法,因为严格空值检查能够减少 JS 代码运行时的一些低级报错。放一张最近我们收到的用户反馈做为旁证:

总结

以上就是此次跟大家介绍的全部内容。感谢大家在论坛里将工作中遇到的问题如实反馈,才让我们有机会整理成文。

如果大家仍有 TypeScript 使用上的问题,可以在本帖回复,也欢迎大家将本文转发给有需要的人!

31赞

typescript真香,
对于C/C艹/C#过来的人尤其香

4赞

TypeScript真香+1 by 一个从C#过来的人

1赞

我只有一个疑问,也是之前1.x,2.x不用typescript的原因:
开发时刷新成preview代码,typescript相对于javascript是否会更慢?慢的话慢多少?
对于比较大型的项目,会不会因为这个转换js的过程导致降低开发效率?

js写起来太头痛了

TypeScript真香+1

1赞

其他都没得问题,唯一问题是不能初始化位Null,引用类型其实默认值为Null是没得问题的 也符合逻辑更符合我们的需求啊

1赞

cocoscreator 什么时候支持 WebAssembly 啊,WebAssembly 比 JavaScript快得多,WebAssembly目前是新的趋势。

是不是今后会在CocosCreator上加入对AssemblyScript 的支持和编译呢?

其实在想,c#编译成的webassembly有许多影响性能的中间代码,typescript(assemblyscript)编译成webassembly是不是也是有许多影响性能的中间代码呢?是不是老老实实用c++或者大胆点用rust编译webassembly比较好呢。。。?

1赞

deno(rust)这条线上的 代替 Electron的GUI环境, TAURI。全改rust的话可以试试,只不过代价太大。。。
https://www.jianshu.com/p/2cf40a2ed366

谢谢反馈,基于我们丰富的 JSB 绑定经验,会妥善处理好胶水代码的。

“| null” 有个非常不好用的地方!当或null之后,vscode就没办法点提示属性了。就比如:
let child: Node | null = instantiate(this.prefab);
child. 点不会提示Node的公有变量方法,很绝望!strictNullChecks: true的完全严格模式话,就必须或null,否则就提示语法错误。严格模式和提示都是提高开发效率减少错误的利器,大人我们怎么做到全都要?@jare

1赞

1.9_CC(6@X0`8UQ{H7~FV~LN0 请问下之前2,4版本是这种代码,现在3.0用不了require了,这个该怎么写
之前require的比如task1.ts task2.ts 这种文件
里面都是类似的export const task = {}
2.之前tween支持bezierTo,现在没这个,该怎么改
@jare

这是一篇好文章,从基础教起,打消js人群对ts的疑虑。

就像我的情况,抵触的不是ts,而是之前写的几十万行js代码(算上回车空格注释),如果想升级,让我有了恐惧症,不知如何下手,自从升级2.2带来了源生渲染大幅提升之后,我就不再相信自定义引擎,所有自定全部都撤销了,使用其他方式实现,也考虑到后期的持续升级版本。

所以对于3.0,我也抱有幻想,毕竟3d的代入感优势太强了,2.X虽然也有3D,但个人不喜欢阉割版,养家的年龄了,做什么都有过日子的心。

“我用了一款特别好的引擎,尽管很多功能没有用到,但我特别有安全感
[如果我想要,它就能满足我]”

在转入游戏开发之前,做java、oc、c++,一直都是强类型语言。直到我遇见了js,才发现这是一门神奇的语言,可以写千百行java不出错,但js就是写几十行就debug一下,看看有没有什么疏漏。

每次开发后台写java都是一种享受,而一写js就开始难受。
[说java也难受的那位,春晚在催你结婚,别回帖子了干点正事]

js的好与不好都太极端了。也可以想象,如果当初cocos用js来写,那个维护可读难度堪称史诗级。

尽管js的缺点很明显也致命,但ts的根也还是js,毕竟ts在编译后还是js,建议在全局设置做一个开关,手动开启可创建js文件(可以隐藏深一些,避免新人再入js的坑)

js作为前端的入门语言,对于cocos吸引这些人群也十分有好处,但如果你是强类型的大佬,也能对口满足。

两手准备两手抓,都那么充实

“什么?你是做网站的?人家可是黄花大闺女!你拿什么养我,养孩子?”
“不!我会js,我还会做游戏,真正的游戏开发”
“那好吧,房子不要了,彩礼也退给你,今晚就办喜事,我图的不是钱,要的是潜力”

“你欺骗了我!我去看了游戏引擎,人家用的明明是ts”
“不,你听我解释,ts就是js,一个强类型一个弱类型,真的你听我解释”
“好啊,原来你是弱类型,我说你看起来怎么这么弱,一看那方面就不行”
“现在黄花大闺女都懂这么多了吗?”
“啊?!啊。。你!你!做技术的都是直男!垃圾!没有情趣!你根本不爱我!分手!”

蝴蝶效应导致拆散了一对恩爱的情侣,生育率的持续下跌的罪魁祸首可能就是cocos引擎组。
【有凭有据】

“我们给你提供了高稳定、高协作、高可读的ts,但如果你想用js,我们也不管你,还给了你一个设置打开入口,允许你作死”

3赞


其余都有深深的同意,这一点实在不敢苟同
两门语言会带来生态割裂??不利用另一门语言的学习?不利于用插件和教程?无法适应ts的项目维护?
不会就去学啊,去习惯啊,怎么能是割裂呢,不是百花齐放么
再一个,选择用哪个语言,这是用户需要思考的问题吧,而不是引擎觉得会怎样怎样。
要做大型游戏,选择了js,自己hold不住只能怪自己啊。那时候没有用户会说引擎怎么不强制我用Ts,都怪引擎。

引擎是不是想的太多了,为大家想的太多了。就像楼上说的,可以用方法来引导新用户用ts,而不是去强制吧。

当然了,我们既然选择了用cocoscreator,你们又(约等于)强制了ts,那没办法只好用了。


如果不推荐使用commonjs的require的话,非得使用动态导入模块的话,是不是引擎组推荐使用es2015的“动态import”呢?
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import