Migration from JavaScript to TypeScript

Heads Up!

This guide is only for developers who want to migrate their existing JavaScript project to TypeScript project. To create a TypeScript project, please refer to Typescript Support guide.

Since Smartface IDE 6.16.0, developers are able to use TypeScript on their projects.

Code Changes on Migration

Minimum Required Versions

To use TypeScript project, minimum supported versions required to launch:

  • sf-core: 4.2.0

  • Smartface Emulator/Player: 6.15.1

It is advised to check your scripts/package.json for versions. They might be outdated!

Tsconfig.json

The tsconfig.json file corresponds to the configuration of the TypeScript compiler (tsc). For more information, refer here Create a file called tsconfig.json on workspace directory and add the following code:

tsconfig.json
tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"skipLibCheck": true,
"sourceMap": true,
"noEmit": false,
"target": "ES5",
"outDir": "./dist",
"moduleResolution": "Node",
"module": "CommonJS",
"resolveJsonModule": true,
"isolatedModules": false,
"typeRoots": [
"./scripts/node_modules/sf-core/device/system"
],
"baseUrl": "scripts",
"rootDir": "scripts",
"lib": [
"ES2015",
"ES2017"
]
},
"files": [
"./typings.d.ts",
"./scripts/node_modules/sf-core/index.d.ts",
"./scripts/node_modules/sf-core/.types/typings.d.ts"
],
"exclude": [
"node_modules",
"./dist",
"**/node_modules/*"
],
"include": [
"./scripts",
"./scripts/**/*.json"
]
}

Typings

Every TypeScript project has mutual typings that to be used globally. Create a file called typings.d.ts on workspace directory and add the following code:

typings.d.ts
typings.d.ts
declare function extend(constructor: () => void): () => FunctionConstructor
declare function require(param:string):any;
declare const lang: {[key:string]: string};
declare const SMF: any;
declare const Device: any;
declare const global: any;
declare const alert: any;
type StyleContextDispatch = {
dispatch: (action: { [key: string]: any }) => void;
}
type StyleContextAddChild = {
addChild(child: View, name?: string, classNames?: string, userProps?: { [key: string]: any; }, defaultClassNames?: string): void;
}
type StyleContextComponent = StyleContextAddChild & StyleContextDispatch;
type StyleContextComponentType<T> = T & StyleContextAddChild & StyleContextDispatch;
type StyleContextComponentWithDispatch<T> = T & StyleContextDispatch;
type componentContextPatch = <T = any>(component: T, name: string) => StyleContextComponentType<T>;

scripts/settings.json

There are a few changes on scripts/settings.json. Newly output property is added to specify generated JavaScript code output. projectType determines if the project is JavaScript or TypeScript. New scripts/settings.json should look like this:

scripts/settings.json
scripts/settings.json
{
"config": {
"theme": {
"currentTheme": "baseTheme",
"baseTheme": "baseTheme",
"themes": [
"defaultTheme",
"baseTheme"
]
},
"paths": {
"pgxFolder": ".ui",
"uiFolder": "scripts/generated/pages",
"pagesFolder": "scripts/pages",
"libraryUiFolder": "scripts/generated/my-components",
"libraryUserFolder": "scripts/components",
"themesFolder": "themes",
"themesDistFolder": "scripts/generated/themes",
"output": {
"root": "dist",
"include": [
"node_modules"
],
"acceptedExtensions": [
".js",
".json",
".map"
]
}
},
"version": "6.15.1",
"projectType": "ts"
}
}

app.js ( app.ts )

Change the file name to app.ts.

Logging TypeScript Errors on Device

Since the project is launched from dist directory now, handing errors and logging them needs an extra step:

  • Add this file to your project error-by-sourcemap.ts

  • Assign newly created file to Application.onUnhandledError

Here is the brief comparison between app.ts and app.js:

app.js
app.ts
app.js
/* globals lang */
require("i18n/i18n.js"); // Generates global lang object
const Application = require("sf-core/application");
const OS = require('sf-core/device/system').OS;
// Set uncaught exception handler, all exceptions that are not caught will
// trigger onUnhandledError callback.
Application.onUnhandledError = function(e) {
alert({
title: e.type || lang.applicationError,
message: OS === "Android" ? e.stack : (e.message + "\n\n*" + e.stack)
});
};
require("sf-extension-utils");
require("./theme");
const router = require("./routes");
router.push("/pages/page1");
app.ts
/* globals lang */
require("i18n/i18n.js"); // Generates global lang object
import Application = require("sf-core/application");
import { errorStackBySourceMap } from "error-by-sourcemap";
import System = require("sf-core/device/system");
// Set uncaught exception handler, all exceptions that are not caught will
// trigger onUnhandledError callback.
Application.onUnhandledError = function (e: UnhandledError) {
const error = errorStackBySourceMap(e);
alert({
title: e.type || lang.applicationError,
message: System.OS === "Android" ? error.stack : (e.message + "\n\n*" + error.stack)
});
};
import "./theme";
require("sf-extension-utils");
const router = require("./routes");
router.push("/pages/page1");

scripts/theme.js ( theme.ts )

Since we have changed theme to import on app.ts:15, we should convert theme.js to theme.ts. An example theme.ts should look like this:

scripts/theme.js
scripts/theme.ts
scripts/theme.js
const Application = require("sf-core/application");
const config = require("./settings.json").config;
const themeConfig = config.theme;
const { createThemeContextBound } = require("@smartface/contx/lib/styling/ThemeContext");
const themeSources = themeConfig.themes
.map(name => ({
name,
rawStyles: require(`./themes/${name}`),
isDefault: themeConfig.currentTheme === name
}));
Application.theme = createThemeContextBound(themeSources);
scripts/theme.ts
import Data = require("sf-core/data");
import Application = require("sf-core/application");
const { config } = require("./settings.json");
const themeConfig = config.theme;
const { createThemeContextBound } = require("@smartface/contx/lib/styling/ThemeContext");
const currentTheme = Data.getStringVariable("currentTheme") || themeConfig.currentTheme;
const { clearCache } = require("sf-extension-utils/lib/getCombinedStyle");
const themeSources = themeConfig.themes
.map(name => ({
name,
rawStyles: require(`./generated/themes/${name}`),
isDefault: currentTheme === name
}));
Application["theme"] = createThemeContextBound(themeSources);
type ThemeListener = (themeName: string) => void;
const themeListeners = new WeakMap<{}, ThemeListener>();
const themeListenerKeys:{}[] = [];
export const ThemeService = {
onChange(listener: ThemeListener) {
const key = {};
themeListenerKeys.push(key)
themeListeners.set(key, listener);
const deletionIndex = themeListenerKeys.length - 1;
return () => {
if(themeListeners.has(key)){
themeListeners.delete(key);
themeListenerKeys.splice(deletionIndex, 1);
}
}
},
changeTheme(name: string) {
Application["theme"]()({
type: "changeTheme",
theme: name
});
clearCache();
themeListenerKeys.forEach((key) => {
if(themeListeners.has(key)){
themeListeners.get(key)(name);
}
})
}
}

scripts/package.json

Update sf-core version matching the minimum required sf-core version. Also add following package: "source-map": "0.6.1" or launch this on terminal: cd scripts/ && npm i --save source-map@0.6.1

Changes on package.json

After saving scripts/package.json file, it is crucial to run npm install in order to update packages on /projects/workspacescripts directory.

.gitignore

Now the generated library, theme and ui files are gathered into scripts/generated folder and the compiled output is gathered into dist folder. Since compiled output is not required to be in the source code, it is recommended to add those on .gitignore file. The example below is how it should look:

.gitignore
.gitignore
.c9/*
.smf/*
.pm2/*
.npm/*
.tmp/*
.templates/*
output/*
plugins/*
!output/Android/README.md
!output/iOS/README.md
!plugins/Android/README.md
!plugins/iOS/README.md
node_modules/
scripts/ui
scripts/themes
scripts/generated
.DS_Store
/packages
dist

Pages

Since JavaScript is not an object oriented language, there is no out-of-the-box way to inherit a class ( namely function ) to be used by different class. Therefore, a module called extend were used to overcome that challenge.

Since TypeScript is an object oriented language, inheritance is handled differently. Therefore, modules like extend needs to be changed to proper classes with inheritance.

Here is the comparison below on a page code. Similar process should be followed on component code.

scrpits/page1.js
scripts/page1.ts
scripts/components/PageTitleLayout.ts
scripts/components/PageTitleLayout.js
scrpits/page1.js
const extend = require('js-base/core/extend');
const Page1Design = require('ui/ui_Page1');
const Page1 = extend(Page1Design)(
function(_super) {
_super(this);
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
);
function onShow(superOnShow) {
superOnShow();
}
function onLoad(superOnLoad) {
superOnLoad();
}
module.exports = Page1;
scripts/page1.ts
import Page1Design from 'generated/pages/page1';
export default class Page1 extends Page1Design {
constructor () {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
}
function onShow(superOnShow) {
superOnShow();
}
function onLoad(superOnLoad) {
superOnLoad();
}
scripts/components/PageTitleLayout.ts
import PageTitleLayoutDesign from 'generated/my-components/PageTitleLayout';
export default class PageTitleLayout extends PageTitleLayoutDesign {
constructor(props?: any, pageName?: string) {
super(props);
}
}
scripts/components/PageTitleLayout.js
const extend = require('js-base/core/extend');
const PageTitleLayoutDesign = require('library/PageTitleLayout');
const PageTitleLayout = extend(PageTitleLayoutDesign)(
function(_super, props = {}, pageName) {
_super(this, props);
}
);
module.exports = PageTitleLayout;

Components

Since constructor on every component has this.pageName = pageName; declaration to determine which page this component is in, a TypeScript class needs to know if such property exists.

Therefore, we can define pageName as a property on class as pageName?: string | undefined;

Disclaimer

Property pageName is to determine which page this component instance belongs to. It is an optional property, hence it might be removed if it feels unnecessary.

To get current page instance on component code, refer to active module located in Smartface Utility Tools.

scripts/components/PageTitleLayout.js
scripts/components/PageTitleLayout.ts
scripts/components/PageTitleLayout.js
const extend = require('js-base/core/extend');
const PageTitleLayoutDesign = require('library/PageTitleLayout');
const PageTitleLayout = extend(PageTitleLayoutDesign)(
function(_super, props = {}, pageName) {
_super(this, props);
this.pageName = pageName;
}
);
module.exports = PageTitleLayout;
scripts/components/PageTitleLayout.ts
import PageTitleLayoutDesign from 'generated/my-components/PageTitleLayout';
export default class PageTitleLayout extends PageTitleLayoutDesign {
pageName?: string | undefined;
constructor(props?: any, pageName?: string) {
super(props);
this.pageName = pageName;
}
}

Using sf-core Properties on TypeScript Files

To obtain intelliSense support on sf-core components, declaring them should be changed to import. For example: old: const System = require('sf-core/system'); new: import System = require('sf-core/system');

To find which files contain sf-core declaration, you can use the Search feature in Smartface IDE and type : require("sf-core to find all the component then change all .ts files declaration as the example above.

scripts/routes/index.js ( scripts/routes/index.ts )

On our converted TypeScript files above, we exported Page component like export default class Page1. Therefore, our Page declarations on newly converted scripts/routes/index.ts should be changed as well.

Add require().default on getPageClass function like below:

scripts/routes/index.ts
scripts/routes/index.ts
...
routes: [
Route.of({
path: "/pages/page1",
build: buildExtender({ getPageClass: () => require("pages/page1").default, headerBarStyle: { visible: true } })
})
]
...
export = router;

Export on TypeScript

For more information, take a look at TypeScript Handbook

Those are bare minimum steps to launch TypeScript on Smartface Multi Channel App.