Why Choose To Use TypeScript?

An informative story was sent to us by a developer at Tencent named Chen Pipi on our Chinese forums. Thanks so much for the story and for allowing us to share it with our English developers.


Outline

1. What is TypeScript

2. The meaning of TypeScript

3. What changes does TypeScript bring?

4. What are the characteristics of TypeScript in Cocos Creator?

5. How to use TS in the original JS project

What is TypeScript 

TypeScript is a cross-platform programming language developed and open-sourced by Microsoft. TypeScript supports all the syntax and semantics of JavaScript and the latest ECMAScript features and many additional features not in JavaScript.

With the TypeScript compiler (tsc), TypeScript code can be compiled into pure and concise JavaScript code.

The meaning of TypeScript

Although TypeScript was born for large-scale projects, it does not mean that it is not suitable for small and medium-sized projects.

TypeScript was born not to replace JavaScript, but to make JavaScript better. Many excellent open-source projects, such as the three front-end frameworks  AngularReact, and  Vue,  have already supported TypeScript. 

Angular2 and Vue 3.0 are directly developed with TypeScript! Most third-party JavaScript libraries provide support for TypeScript. And Deno 1.0, recently officially released by the author of Node.js, also natively supports TypeScript.

Currently Creator 3.0 only supports creating TypeScript within the editor, and the Cocos Creator engine development team also recommends developers to use TypeScript for development.

What changes does TypeScript bring

When using JavaScript for development, since there are no type restrictions, auto-completion, and smart promptsfrequent communication between developers or frequent reading of documents (detailed documents are critical) is required to ensure that the code can be executed correctly.

When using TypeScript for development, thanks to the type systemautomatic completion is available when reading variables or calling functions, basically preventing incorrect variable/function names from being written. Type restrictions and smart prompts allow developers to quickly know the parameter requirements when calling APIs, without the need to frequently read code, documents, or ask module developers for help. All variables, functions, and classes can be traced quickly (jump to definition), which makes TypeScript code more maintainable

Type system

As we all know, JavaScript is a weakly typed language, and the type of variables cannot be determined until execution. When coding, you can do whatever you want without reporting an error, which may lead to  unexpected bugs.

TypeScript, as a strongly typed language, has a static type checking feature that allows developers to avoid most type errors when coding, and all they have to do is write one more symbol and one more word when declaring variables.

let name: String = 'Chen Pipi';
name = 9527; // error report
 
let age: any = 18;
age = 'eighteen'; // no error report

It achieves early detectionearly resolution, and an early clock out at the end of the day.

In addition to supporting almost the same primitive types as JS, TS also provides additional support for enumerations (Enum) and tuples (Tuple). (No need for cc.Enum anymore)

// enumerate
enum Direction {
     Up = 1 ,
     Down,
     Left,
     Right
 }
let direction: Direction = Direction.Up;
 
// tuple
let  x: [ string , number ];
x = [ 'hello' , 10 ]; / / No error is reported
x = [ 10 , 'hello' ]; // Error is reported

In addition, the type system cooperates with the declaration file. It brings us the perfect auto-completion smart prompt in the editor, which significantly increases the development efficiency and no longer causes run-time errors due to misspelled variable names or function names. (I know that JS plug-ins can also achieve a certain degree of intelligent prompts, but is it good to have to go that far for prompts?)

Modifiers and static keywords

The following is a set of modifiers and keywords that have been lifted from C# almost intact:

Access modifiers: public, private, and protected are used to limit the accessible scope of class members. I feel that the encapsulation without access modifiers is soulless!

class Me {
     public  name = 'Chen Pipi' ; // Everyone knows my name is Chen Pipi
     private  secret = '*******' ; // Only I know my secret
     protected  password = '***** ***' ; // My Alipay password will tell my descendants
 }
 
let me = new Me();
let a = me.name; // I got my name
let b = me.secret; // Error, private property
let c = me.password; // Error, protected property
 
class Child extends  Me {
     constructor () {
         super ();
         this.name = 'Chen XX';
         this.secret // Error, unable to access
         this.password = '888888' ; // Accessible
     }
 }

Static keywordsstatic isused to define globally unique static variables and static functions. ( In the JS script of Creator, the static attribute of cc.Class is used to define static members.)

class Whatever {
     public static origin: String = 'Whatever' ;
     public static printOrigin() {
         console.log(this.origin);
         console.log(Whatever.origin);
     };
 }
 
console.log(Whatever.origin); / / Whatever
Whatever.printOrigin(); // Whatever

Abstract keywordsabstract isused to define abstract classes or abstract functions, an essential part of object-oriented programming.

abstract class Animal  {
     abstract  eat () : void ; // Different animals eat differently
}
 
let animal = new  Animal(); // If an error is reported, there is no
 
class Dog implements  Animal  {
     eat() {
         console.log( 'I eat, woof!' );
     }
}
 
let dog = new Dog();
dog.eat(); // I eat, woof!
 
class Cat implements Animal {
     // An error is reported, the function of eating is not implemented
 }

Read-only keywordsreadonly is used to define a read-only field so that the field can only be assigned once when it is created.

class  Human {
     name: String ;
     readonly id: number ;
     constructor ( name: String , id: number ) {
         this.name = name;
         this.id = id;
     }
 }
 
 let human = new Human( 'Chen Pipi' , 666666 );
 human.name = 'Chen Pipi '; // name can be changed
 human.id = 999999; // error, ID number cannot be changed once confirmed

Interface

Interface is used to declare a series of members but does not contain implementation. The interface supports merging (repetitive declarations) and can also be inherited from another interface. Here are a few common usages:

Extend primitive types

// Extend the String type
interface String {
 
     /**
      * Translation
      */
     translate(): string ;
 
}
 
// Implement the translation function
String.prototype.translate = function  () {
     return this ; // If you don't write it specifically, return directly to the original String bar
 };
 
// use
let  nickname = 'Chen Pipi'.translate();

Definition types

interface  Human {
     name:String ; // Common attributes, must have but can be changed
     readonly id:number ; // Read-only attributes, once confirmed, you can't change
     hair?:number ; // Optional attributes, pretty bald
 }
 
let ChenPiPi: Human = {
     name: 'Chen Pipi' ,
     id: 123456789 ,
     hair: 9999999999999
 }

Class implementation interfaces

interface Vehicle {
     wheel:number;
     engine?:String;
     run():void;
 }
 
class Car implements Vehicle {
     wheel: 4;
     engine: 'Emperor Engine';
     run() {
         console.log( 'The car runs fast !' )
     }
 }
 
class  Bike implements  Vehicle {
     wheel: 2;
     run() {
         console.log( 'Little yellow car rushing!' )
     }
 }

Type alias

This is a more commonly used feature, as its name suggests. Type alias is used to give a new name to the type. Type is very similar to the interface. Type can act on primitive types, union types, tuples, and any other types that you need to write by yourself. Interface supports merging, but type cannot. Type also supports extensions and can be extended with interfaces.

// Give the original type a small name
type UserName = string;
let userName: UserName = 'Chen Pi';
 
// It can also be a function
type GetString = () => string;
let getString: GetString = () => {
     return  ' i am string';
 }
let result = getString();
 
// Create a new type
type Name = {
     realname:String;
     nickname:String;
}
let name: Name = {
     realname: 'Daniel Wu' ,
     nickname: 'Chen Pipi'
}
// Here is another new type
type Age = {
     age: number ;
}
// Use the above two types to extend a new type
type User = Name & Age;
let user: User = {
     realname: 'Daniel Wu',
     nickname: 'Chen Pipi',
     age: 18,
}

Union type

Using Union Types allows you to be compatible with multiple types when declaring variables or receiving parameters. It’s one of my favorite features!

Indicates that a value can be one of several types

let bye: string | number;
bye = 886; // no error
bye = 'bye'; // no error
bye = false; // error

Let the function accept different types of parameters and do different processing inside the function

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); // returns 'Hello world'
padLeft('Hello' , 'I said:'); // returns 'I said: Hello'

Generics

Using generics allows a class/function to support multiple types of data, and you can pass in the required type when using it. It is also a convenient feature. The use of generics can significantly increase the reusability of the code and reduce repetitive work. Like the following are two common usages:

Generic functions

// This is a function for cleaning items
function wash < T >( item: T ): T  {
     // Pretend to have cleaning logic...
     return  item;
}
 
class Dish {} // This is the plate
let dish = new Dish (); // Here is a plate
// The plate is still a plate after washing.
// Use angle brackets to tell it in advance that this is a plate
dish = wash<Dish>(dish);
 
class Car {} // This is a car
let car = new Car (); // Buy a car
// The car is still a car after washing.
// I didn't tell it that was a car but it recognized
car = wash(car);

Generic classes

// Box
class Box<T>{
     item: T = null;
     put(value: T) {
         this.item = value;
     }
     get () {
         return this.item;
     }
}
 
let stringBox = new Box< String >(); // Buy a box to hold String
stringBox.put( 'Hello!' ); // Save a 'Hello!'
// stringBox.put(666); // An error is reported, only String types can be stored
let string = stringBox.get(); // String type is taken out

Decorator

This is a relatively advanced feature. Additional declarations of classes, functions, accessors, attributes, or parameters are made in the form of @expression. Use a decorator to do a lot of operations, and if you are interested, you can study it in depth.

Preprocessing the class

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); // white
 
 @color ( 'red' )
 class Car {
     color: String;
 }
 let car = new Car();
 console.log(car.color); // red

The cc.class and property in the TS component in Creator are two decorators

const {ccclass, property} = cc._decorator;
 
@ccclass
export default class CP extends cc.Component {
 
     @property (cc.Node)
     private abc: cc.Node =  null;
 
 }

Namespaces

Namespace is used to define the range of available identifiers, mainly used to solve the problem of duplicate names, and are useful for project modularity. The cc in Cocos Creator is a built-in namespace.

Distinguish the classes and functions of the same name

// pp namespace
namespace pp {
     export class Action  {
         public static speak () {
             cc.log( 'I am pp!' );
         }
     }
}
 
// dd
namespace namespace dd {
     export class Action {
         public static speak ( ) {
             cc.log( 'I am dd!' );
         }
     }
}
 
// Use
pp.Action.speak(); // I am pp!
dd.Action.speak(); // I am dd!

Classify the interface

namespace  Lobby  {
     export interface  Request  {
         event : String,
         other: object
         // ...
     }
}
 
namespace  Game  {
     export interface  Request  {
         event : String,
         status: String
         // ...
     }
}
 
// Request function for Lobby
function requestLobby ( request: Lobby.Request ) {
     // ...
}
 
// Request function for Game
functionrequestGame ( request: Game.Request ) {
      // ...
}

Declaration file

Declaration Files, which are code files with d.ts as the suffix, are used to declare the types available in the current environment.
The feature of the declaration file is extremely important for TypeScript. The features such as smart prompts in the code editor all depend on the declaration file.

It can be found that most third-party JavaScript libraries currently have declaration files. The declaration files allow these libraries to have features such as smart prompts for type checking in the code editor.


We can even declare some types that do not exist in the environment. For example, the wx.d.ts file I wrote in the article “WeChat Mini Game Access Friends Ranking” , so that I can call non-existent wx functions in the editor environment without errors and with smart prompts.

Generally, there is a declaration file creator.d.ts in the root directory of the Cocos Creator project, which declares almost all available APIs of the Cocos Creator engine. So even if it is a pure JavaScript Creator project, there are smart tips when using the cc namespace.

The difference between TS and JS in Creator

Declare components

The declaration method of a class in TypeScript script is similar to ES6 Class, and the decorator @ccclass is used to declare an ordinary class as CCClass:

const {ccclass} = cc._decorator;
 
@ccclass
export  default  class  Test  extends  cc.Component {

}

The way to declare in JavaScript script :

cc.Class ({
     extends : cc.Component,
});

Declare properties

In TypeScript scripts, you need to use the decorator  @property  to declare properties, and the basic types need not pass parameters (parameters are basically the same as when using JavaScript) :

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: 'location',
         tooltip: 'is a location'
     })
     myVec2: cc.Vec2 = new cc.Vec2();
 
     @property ({
         type : cc.Sprite,
         visible() { return this.myBoolean },
         tooltip: 'The property will only be displayed when myBoolean is true'
     })
     mySprite: cc.Sprite = null;
 
     @property
     _getset = 0;
     @property
     get getset() { return  this._getset}
     set getset(value) { this._getset = value }
 
}

In the JavaScript script, you need to define the properties in “properties” (there is no smart prompt when you using it) :

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: 'location',
             tooltip: 'is a location'
         },
         mySprite:{
             type: cc.Sprite,
             default : null,
             visible() {return  this.myBoolean },
             tooltip: 'The property will only be displayed when myBoolean is true'
         },
         _getset: 0,
         getset:{
             get () { return  this._getset;},
             set ( value ) { this._getset = value ;}
         }
     }
 
 });

Import/export components/modules

Use ES modules in  TypeScript scripts to export or import components/modules:

// A.ts
const {ccclass, property} = cc._decorator;
 
@ccclass
export default class A extends cc.Component {
 
     @property
     public nickname:string  = 'A';
 
     public greet() {
         cc.log( 'A: greet()' );
     }
 
}
 
 // B.ts
import A from "./A";
 
const {ccclass, property} = cc._decorator;
 
@ccclass
export default class B extends cc.Component {
 
     @property (A)
     private  a: A = null;
 
     onLoad() {
         // access instance properties
         let nickname = this.a.nickname;
         // call instance function
         this.a.greet();
     }
 
}

The Common JS module method is used in JavaScript script (in fact, cc.Class will be exported by default, but VS Code can’t recognize it, so it will usually be exported with module.export):

// A.js
let  A = cc.Class({
     extends : cc.Component,
 
     properties: {
         nickname: 'A'
     },
 
     greet() {
         cc.log( 'A: greet()' );
     }
 
});
 
module.export  = A;
 
// B.js
let  A = require ( './A' );
 
let  B = cc.Class({
     extends: cc.Component,
 
     properties: {
         a: {
             type : A,
             default: null
         }
     },
 
     onLoad() {
         // Access instance properties
         let nickname = this.a.nickname;
         // call the instance function
         this.a.greet();
     }
  
});
 
module.export  = B;

Static variable/function

Use the static keyword directly to declare static variables and functions in TypeScript scripts  :

// A.ts
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( )' );
     }
 
}
 
// B.ts
import A from "./A";
 
const {ccclass, property} = cc._decorator;
 
@ccclass
export default class B extends cc.Component {
 
     onLoad() {
         // access static properties
         let id = A.id;
         // call static function
         A.staticGreet();
     }
 
}

Use the static property to define static variables or functions in JavaScript scripts:

// A.js
let  A = cc.Class({
     extends : cc.Component,
 
     static : {
         id: 999999,
 
         staticGreet() {
             cc.log( 'A: staticGreet()');
         }
     }
 
});
 
 module.export = A;
 
 // B.js
 let A = require ( './A' );
 
 let B = cc.Class({
     extends: cc.Component,
 
     onLoad() {
         // Access static variable
         let id = A.id;
         // call static function
         A.staticGreet();
     }
 
 });
 
 module.export  = B;

Enumerate

The above also mentioned that TS comes with enumeration types so you can use enum directly in TS scripts to define enums, while cc.Enum needs to be used in JS script to define enumeration.

// TypeScript script method
enum  Direction {
     Up = 1,
     Down,
     Left,
     Right
 }
 
// JavaScript script method
const  Direction = cc.Enum ({
     Up = 1 ,
     Down,
     Left,
     Right
});

How to use TS in the original JS project

In 2.x version:

You need to click  [ Developer -> VS Code Workflow -> Update VS Code Smart Prompt Data]  and [ Developer -> VS Code Workflow -> on the main menu above the editor Add TypeScript project configuration]  to add creator.d.ts  declaration file and tsconfig.json  configuration file to the project.

  • creator.d.ts is the declaration file of the Cocos Creator engine API
  • tsconfig.json is the environment configuration file of the TypeScript project

In 3.0 version:

You just need to click File -> Import Cocos Creator 2.x project in the menu bar.

This tool supports perfect import of assets from old projects and code assisted migration. Code assisted migration converts JavaScript to TypeScript and automatically adds component type declarations, attribute declarations and function declarations. The references to components in scenarios are preserved, and code inside functions is migrated as comments, which can reduce the difficulty of upgrading.

Pin It on Pinterest