本文最后更新于:2020年5月18日 凌晨
前言
相信经常关注前端技术的同学对 TypeScript 应该不陌生,或多或少看过一些关于 TypeScript 的文章。
各种技术论坛上也有不少关于 TypeScript 和 JavaScript 的讨论,大多数人对 TypeScript 都有着不错评价,但也有不少人觉得它没有存在的必要。
事实上,TypeScript 作为前端编程语言界的当红炸子鸡,配合代码编辑器界的当红炸子鸡 VS Code 一起食用,能够让人拥有极佳的用餐哦不编码体验。
许多过去一直使用 JavaScript 的同学,在使用 TypeScript 之后,都觉得再也回不去了。微软的这一套组合拳,打得多少人大喊真香!(真香定律虽迟但到)
它不是好不好用的问题,它真的是那种,那种很少见的那种……

鲁迅先生曾说过:人生苦短,我用 TS 。
回到正题
作为假前端的我,使用 TypeScript 进行开发也有近两年的时间了,也希望和各位分享一下我的看法。
所以在本篇文章我将以一名 Cocos Creator 开发者的角度,来对 TypeScript 做一波客观分析(强行安利),希望对各位有所帮助。
大纲
1. 什么是 TypeScript
2. TypeScript 存在的意义
3. TypeScript 带来了什么改变
4. TypeScript 有什么特性
5. Cocos Creator 中 TS 和 JS 在使用上的区别
6. 如何创建 Cocos Creator TS 项目
7. 原有的 JS 项目如何使用 TS
正文
什么是 TypeScript

TypeScript 是一种由微软开发并开源的跨平台编程语言,最初开发 TypeScript 的目的是为了更好地开发大型项目,其作者为大名鼎鼎的 C# 之父 Anders Hejlsberg 。
在 TypeScript 中文主页中对于 TypeScript 的定义是“JavaScript 的超集”, TypeScript 支持JavaScript 的所有语法语义和最新的 ECMAScript 特性,并且额外添加了很多特性。
通过 TypeScript 编译器(tsc),TypeScript 代码可以被编译成纯净、简洁的 JavaScript 代码。
主页中对 TypeScript 的介绍:

TypeScript 存在的意义
生产力工具
TypeScript 虽为大型项目而生,但是不代表它不适用于中小型项目,只是项目越大收益越明显。
TypeScript 弥补了 JavaScript 的许多不足,同时保留了 JavaScript 的灵活性,大大提高了项目的开发效率以及可维护性。
TypeScript 的诞生不是为了取代 JavaScript ,而是让 JavaScript 变得更好。
所以 TypeScript 对于开发者来说,不仅仅是一门编程语言,更是生产力工具。
前途大好
TypeScript 的良好口碑以及日渐庞大的生态,早就已经证明了它自己。
许多优秀的开源项目例如前端三大框架 Angular、React 和 Vue 均已支持 TypeScript ,Angular2 和 Vue 3.0 都是直接用 TypeScript 开发的!
大多数第三方 JavaScript 库都提供了对 TypeScript 的支持。
并且 Node.js 作者近期正式发布的 Deno 1.0 也是原生支持 TypeScript 。
可以看到 TypeScript 的未来一片光明…
你几乎天天用来写代码的 VS Code 也是用 TypeScript 编写的。(用记事本写代码的大佬请先收起你的菜刀)
Cocos Creator
而对于 Creator 开发者来说最最最重要的是:
Cocos Creator 引擎开发团队也建议开发者使用 TypeScript 进行开发。
目前 Creator 3D 只支持使用 TypeScript 进行开发。
我可以大胆的说未来 TypeScript 将会成为 Cocos Creator 开发的标配!
TypeScript 带来了什么改变
既然 TypeScript 为大型项目而生,那不如就让我们看看 TypeScript 为什么适合大型项目?
在项目的开发中,必定少不了众多的开发人员,在这个模块化的时代,一个项目的多个模块可能均由不同的人来开发,并且每个人都有不同的编码习惯。
在使用 JavaScript 进行开发时,由于没有类型限制、自动补全和智能提示,就需要开发人员之间的频繁沟通或者频繁阅读文档(详细的文档很关键)来保证代码可以正确执行。
即便如此,开发人员也不能保证每个变量/函数名都一次写对,每个参数都一次传对。
这些沟通和翻阅文档所花费的时间都在默默降低项目的整体开发效率。
而使用 TypeScript 进行开发时,得益于类型系统,在读取变量或调用函数时,均有自动补全,基本杜绝写错变量/函数名的情况。
类型限制与智能提示让开发人员调用 API 时可以快速得知参数要求,不需要再频繁阅读代码、文档或询问模块开发者。
所有变量、函数和类都可以快速溯源(跳转到定义),让 TypeScript 代码有着较好的可维护性。合理利用注释甚至可以完全不看文档,真正做到“注释即文档”(文档还是要有的 : p)。
总之就是开发效率 +++ !
TypeScript 的特性
类型系统
众所周知 JS 是一门弱类型语言,不到执行时都不能确定变量的类型。编码时可以随心所欲反正不报错,一不小心就写出八哥( undefined 警告!)。
1. 静态类型检查
静态类型检查让 TS 在编辑器中披上强类型语言的“马甲”,使得开发者在编码时就可以避免大多数类型错误的情况发生,而开发者要做的就只是声明变量时多写一个符号和一个单词。
当然你也可以在声明变量时不指定类型或者使用 any 类型来达到 JS 的动态类型效果,让 TypeScript 变成 AnyScript ,任性~
1 2 3 4 5
| let name: string = '陈皮皮'; name = 9527;
let age: any = 18; age = 'eighteen';
|
真正做到 早发现,早解决,早下班
2. 原始类型
TS 在支持与 JS 基本相同的原始类型之外,还额外提供了枚举(Enum)和元组(Tuple)的支持。
Cocos Creator 用户狂喜:再也不需要 cc.Enum 了 ; p
1 2 3 4 5 6 7 8 9 10 11 12 13
| enum Direction { Up = 1, Down, Left, Right } let direction: Direction = Direction.Up;
let x: [string, number]; x = ['hello', 10]; x = [10, 'hello'];
|
3. 智能提示
类型系统配合声明文件(关于声明文件我们后面再聊)给我们带来了编辑器中完善的自动补全智能提示,大大增加了开发效率,也再不会因为拼错变量名或函数名而导致运行时的错误。
我知道 JS 加插件也能实现一定程度的智能提示但是语言自带它不香吗 : )
修饰符和静态关键字
泪目,是从 C# 那里几乎原汁原味搬过来的一套修饰符和关键字,主要如以下几个:
1. 访问修饰符(public、private 和 protected)
用来限定类成员的可访问范围。
没有 internal 和 protect internal
没有访问修饰符的封装莫得灵魂!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Me { public name = '陈皮皮'; private secret = '*******'; protected password = '********'; }
let me = new Me(); let a = me.name; let b = me.secret; let c = me.password;
class Child extends Me { constructor() { super(); this.name = '陈XX'; this.secret this.password = '888888'; } }
|
2. 静态关键字(static)
用于定义全局唯一的静态变量和静态函数。
在 Creator 的 JS 脚本中是使用 cc.Class 的 statics 属性来定义静态成员的,使用体验一言难尽…
另外在 ES6 中 JS 已经支持静态函数,在 ES7 中也加入了对静态属性的支持。
1 2 3 4 5 6 7 8 9 10
| class Whatever { public static origin: string = 'Whatever'; public static printOrigin() { console.log(this.origin); console.log(Whatever.origin); }; }
console.log(Whatever.origin); Whatever.printOrigin();
|
3. 抽象关键字(abstract)
用来定义抽象类或抽象函数,面向对象编程很重要的一环。
没对象的可以面向工资编程…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| abstract class Animal { abstract eat(): void; }
let animal = new Animal();
class Dog implements Animal { eat() { console.log('我吃,汪!'); } }
let dog = new Dog(); dog.eat();
class Cat implements Animal { }
|
4. 只读关键字(readonly)
用来定义只读的字段,使得字段只能在创建的时候赋值一次。
1 2 3 4 5 6 7 8 9 10 11 12
| class Human { name: string; readonly id: number; constructor(name: string, id: number) { this.name = name; this.id = id; } }
let human = new Human('陈皮皮', 666666); human.name = '陈不皮'; human.id = 999999;
|
接口(Interface)
C# 和 Java 的朋友们让我看到你们的双手好吗
接口用于一系列成员的声明,但不包含实现,接口支持合并(重复声明),也可以继承于另一接口。
下面展示几个常见的用法:
1. 扩展原始类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| interface String {
translate(): string;
}
String.prototype.translate = function () { return this; };
let nickname = '陈皮皮'.translate();
|
2. 定义类型
1 2 3 4 5 6 7 8 9 10 11
| interface Human { name: string; readonly id: number; hair?: number; }
let ChenPiPi: Human = { name: '陈皮皮', id: 123456789, hair: 9999999999999 }
|
3. 类实现接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| interface Vehicle { wheel: number; engine?: string; run(): void; }
class Car implements Vehicle { wheel: 4; engine: '帝皇引擎'; run() { console.log('小汽车跑得快!') } }
class Bike implements Vehicle { wheel: 2; run() { console.log('小黄车冲冲冲!') } }
|
类型别名(Type)
这是一个比较常用的特性,作用如其名。
类型别名用来给类型起一个新的名字。
类型别名和接口很相似,类型别名可以作用于原始类型,联合类型,元组以及其它任何你需要手写的类型,接口支持合并而类型别名不可以。
类型别名同样也支持扩展,并且可以和接口互相扩展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| type UserName = string; let userName: UserName = '陈皮';
type GetString = () => string; let getString: GetString = () => { return 'i am string'; } let result = getString();
type Name = { realname: string; nickname: string; } let name: Name = { realname: '吴彦祖', nickname: '陈皮皮' }
type Age = { age: number; }
type User = Name & Age; let user: User = { realname: '吴彦祖', nickname: '陈皮皮', age: 18, }
|
联合类型(Union Types)
使用联合类型允许你在声明变量或接收参数时兼容多种类型。
个人最喜欢的特性之一,点赞!
1. 表示一个值可以是几种类型之一
1 2 3 4
| let bye: string | number; bye = 886; bye = 'bye'; bye = false;
|
2. 让函数接受不同类型的参数,并在函数内部做不同处理
1 2 3 4 5 6 7 8 9 10
| function padLeft(value: string, padding: string | number) { if (typeof padding === 'string') { return padding + value; } else { return Array(padding + 1).join('') + value; } }
padLeft('Hello world', 4); padLeft('Hello', 'I said: ');
|
泛型(Generics)
C# 和 Java 的朋友们再次让我看到你们的双手好吗
使用泛型可以让一个类/函数支持多种类型的数据,使用时可以传入需要的类型。
又是一个非常实用的特性,利用泛型可以大大增加代码的可重用性,减少重复的工作,点赞!
以下是两个常用的用法:
1. 泛型函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function wash<T>(item: T): T { return item; }
class Dish { } let dish = new Dish();
dish = wash<Dish>(dish);
class Car { } let car = new Car();
car = wash(car);
|
2. 泛型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Box<T>{ item: T = null; put(value: T) { this.item = value; } get() { return this.item; } }
let stringBox = new Box<String>(); stringBox.put('你好!');
let string = stringBox.get();
|
装饰器(Decorator)
这是一个相对比较高级的特性,以 @expression 的形式对类、函数、访问符、属性或参数进行额外的声明。
利用装饰器可以做很多骚操作,感兴趣的话可以深入研究下。
对类做预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export function color(color: string) { return function (target: Function) { target.prototype.color = color; } }
@color('white') class Cloth { color: string; } let cloth = new Cloth(); console.log(cloth.color);
@color('red') class Car { color: string; } let car = new Car(); console.log(car.color);
|
Creator 中的 TS 组件中的 ccclass 和 property 就是两个装饰器
1 2 3 4 5 6 7 8 9
| const { ccclass, property } = cc._decorator;
@ccclass export default class CPP extends cc.Component {
@property(cc.Node) private abc: cc.Node = null;
}
|
命名空间(namespace)
命名空间用来定义标识符的可用范围,主要用于解决重名的问题,对于项目模块化有很大的帮助。
Cocos Creator 中的 cc 就是一个内置的命名空间。
1. 对相同名字的类和函数进行区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| namespace pp { export class Action { public static speak() { cc.log('我是皮皮!'); } } }
namespace dd { export class Action { public static speak() { cc.log('我是弟弟!'); } } }
pp.Action.speak(); dd.Action.speak();
|
2. 对接口进行分类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| namespace Lobby { export interface Request { event: string, other: object } }
namespace Game { export interface Request { event: string, status: string } }
function requestLobby(request: Lobby.Request) { }
function requestGame(request: Game.Request) { }
|
声明文件(Declaration Files)
声明文件,即以 d.ts 作为后缀的代码文件,用来声明当前环境中可用的类型。
声明文件这一特性对于 TypeScript 来说是极其重要的,代码编辑器中的智能提示等特性都依赖于声明文件。
可以发现目前大多数第三方 JavaScript 库都有声明文件,声明文件让这些库在代码编辑器中也可以拥有类型检查智能提示等特性,使用体验 Max 。
我们甚至可以声明一些环境中不存在的类型,例如我在《微信小游戏接入好友排行榜》这篇文章中编写的 wx.d.ts 文件,使得我在编辑器环境中调用根本不存在的 wx 函数时不会报错且有智能提示。
一般 Cocos Creator 项目的根目录下都有一个声明文件 creator.d.ts ,文件中声明了 Cocos Creator 引擎几乎所有可用的 API 。所以即使是纯 JavaScript 的 Creator 项目,使用 cc 命名空间时也有智能提示。

Creator 中 TS 和 JS 在使用上的区别
声明组件
在 TypeScript 脚本中 class 的声明方式 和 ES6 Class 相似,并使用了装饰器 @ccclass 来将普通 class 声明成 CCClass :
1 2 3 4 5 6
| const { ccclass } = cc._decorator;
@ccclass export default class Test extends cc.Component {
}
|
在 JavaScript 脚本中声明的方式:
1 2 3 4
| cc.Class({ extends: cc.Component,
});
|
声明属性
在 TypeScript 脚本中需要使用装饰器 @property 来声明属性,基本类型可以不传参数(参数和使用 JavaScript 时基本一致):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| const { ccclass, property } = cc._decorator;
@ccclass export default class Test extends cc.Component {
@property myNumber: number = 666;
@property myString: string = '666';
@property myBoolean: boolean = true;
@property(cc.Node) myNode: cc.Node = null;
@property([cc.Node]) myNodes: cc.Node[] = [];
@property({ visible: true, displayName: '位置', tooltip: '就是一个位置' }) myVec2: cc.Vec2 = new cc.Vec2();
@property({ type: cc.Sprite, visible() { return this.myBoolean }, tooltip: '当 myBoolean 为 true 才会展示该属性' }) mySprite: cc.Sprite = null;
@property _getset = 0; @property get getset() { return this._getset } set getset(value) { this._getset = value }
}
|
在 JavaScript 脚本中需要在 properties 中定义属性(使用时没有智能提示,就很难受):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| cc.Class({ extends: cc.Component,
properties: { myNumber: 666, myString: '666', myBoolean: true, myNode: cc.Node, myNodes: [cc.Node], myVec2: { default: new cc.Vec2(), visible: true, displayName: '位置', tooltip: '就是一个位置' }, mySprite: { type: cc.Sprite, default: null, visible() { return this.myBoolean }, tooltip: '当 myBoolean 为 true 才会展示该属性' }, _getset: 0, getset: { get() { return this._getset; }, set(value) { this._getset = value; } } }
});
|
导入/导出组件/模块
在 TypeScript 脚本中使用 ES 模块的方式来导出或导入组件/模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const { ccclass, property } = cc._decorator;
@ccclass export default class A extends cc.Component {
@property public nickname: string = 'A';
public greet() { cc.log('A: greet()'); }
}
import A from "./A";
const { ccclass, property } = cc._decorator;
@ccclass export default class B extends cc.Component {
@property(A) private a: A = null;
onLoad() { let nickname = this.a.nickname; this.a.greet(); }
}
|
在 JavaScript 脚本中默认使用的是 Common JS 模块的方式
其实 cc.Class 会默认导出,但是 VS Code 识别不了,且使用时也没有智能提示全靠手打
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| let A = cc.Class({ extends: cc.Component,
properties: { nickname: 'A' },
greet() { cc.log('A: greet()'); }
});
module.export = A;
let A = require('./A');
let B = cc.Class({ extends: cc.Component,
properties: { a: { type: A, default: null } },
onLoad() { let nickname = this.a.nickname; this.a.greet(); }
});
module.export = B;
|
静态变量/函数
在 TypeScript 脚本中直接使用 static 关键字声明静态变量和函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const { ccclass, property } = cc._decorator;
@ccclass export default class A extends cc.Component {
public static id: number = 999999;
public static staticGreet() { cc.log('A: staticGreet()'); }
}
import A from "./A";
const { ccclass, property } = cc._decorator;
@ccclass export default class B extends cc.Component {
onLoad() { let id = A.id; A.staticGreet(); }
}
|
在 JavaScript 脚本中使用 statics 属性来定义静态变量或函数(使用时没有智能提示全靠手打):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| let A = cc.Class({ extends: cc.Component,
statics: { id: 999999,
staticGreet() { cc.log('A: staticGreet()'); } }
});
module.export = A;
let A = require('./A');
let B = cc.Class({ extends: cc.Component,
onLoad() { let id = A.id; A.staticGreet(); }
});
module.export = B;
|
枚举
上面也有说到 TypeScript 自带枚举类型,所以在 TypeScript 脚本中可以直接用 enum 关键字来定义枚举,而在 JavaScript 脚本中需要用 cc.Enum 来定义枚举。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| enum Direction { Up = 1, Down, Left, Right }
const Direction = cc.Enum({ Up = 1, Down, Left, Right });
|
如何创建 Creator TS 项目
新建项目时,在项目模板中选择 Hello TypeScript ,就可以创建一个含有 TypeScript 相关配置和基本组件的项目。

原有的 JS 项目使用 TS
添加配置
想要在原有的 JavaScript Creator 项目中使用 TypeScript ,需要点击编辑器上方主菜单的 [开发者 -> VS Code 工作流 -> 更新 VS Code 智能提示数据] 和 [开发者 -> VS Code 工作流 -> 添加 TypeScript 项目配置] 来给项目添加 creator.d.ts 声明文件和 tsconfig.json 配置文件。
- creator.d.ts 是 Cocos Creator 引擎 API 的声明文件
- tsconfig.json 是 TypeScript 项目的环境配置文件

混用
在 Creator 项目中添加配置后可以混用 JS 和 TS 脚本,也能享受到 TS 到来的福利。也就是说原有的 JS 脚本可以保留,不影响后续添加新的 TS 脚本。
重构
但是如果想要将项目完全重构为 TS 项目,要做的就是将原有的 JS 脚本逐个修改为 TS 脚本,并对脚本内的写法进行转换。
对于较为复杂的项目,对项目代码进行重构这一行为可能需要花费较长的时间,如果没有做好充足的准备,不建议着手进行。
但是一旦完成重构,TS 绝不会让你失望,必定会给项目开发带来全方位的提升!
相关资料
TypeScript 官网
https://www.typescriptlang.org
TypeScript 中文网
https://www.tslang.cn
TypeScript 开源代码仓库
https://github.com/Microsoft/TypeScript
Cocos Creator TypeScript 使用文档
https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
TypeScript 入门教程 by xcatliu
https://github.com/xcatliu/typescript-tutorial
ECMAScript 6 入门 by 阮一峰
https://es6.ruanyifeng.com
awesome-typescript by semlinker
https://github.com/semlinker/awesome-typescript
传送门
微信推文版本
个人博客:菜鸟小栈
开源主页:陈皮皮
Eazax Cocos 游戏开发工具包
更多分享
多平台通用的屏幕分辨率适配方案
围绕物体旋转的方案以及现成的组件
一个全能的挖孔 Shader
一个开源的自动代码混淆插件
微信小游戏接入好友排行榜(开放数据域)
公众号
菜鸟小栈
我是陈皮皮,这是我的个人公众号,专注但不仅限于游戏开发、前端和后端技术记录与分享。
每一篇原创都非常用心,你的关注就是我原创的动力!
Input and output.
