typescript

Primeros pasos con typescript en UI5

28 noviembre 2022 · 7 minutos de lectura

A día de hoy no empezaría ningún proyecto UI5 en javascript. Utilizar typescript supone una serie de ventajas que hace que ni me plantee la idea de no considerarlo. Evidentemente, hay que ajustarse al proyecto y al cliente, ya que es posible que haya cierto rechazo. Pero hay que tener en cuenta una cosa: un proyecto hecho con typescript implica que a su vez podemos hacer un entregable de código fuente en javascript.

La idea es la siguiente. Un proyecto UI5 tiene tradicionalmente, en su carpeta raíz, una carpeta principal llamada webapp. Es donde tendremos el código javascript. Una vez queremos crear el distribuible, el código se empaqueta en la carpeta dist. Por lo tanto, el flujo del código va desde webapp -> dist.

Bien, pues con typescript añadiremos una carpeta nueva, que llamaremos src, y será la carpeta principal del proyecto donde tendremos el código typescript. Para poder ejecutar ese código en el navegador mientras desarrollamos, esta carpeta se irá transpilando automáticamente a javascript en la carpeta webapp. El código que tendrá ahora esta carpeta será prácticamente idéntico al código que hubiésemos escrito nosotros mismos si hiciésemos el proyecto en javascript. Por lo tanto, el flujo quedará así: src -> webapp -> dist.

Con este flujo, tenemos dos carpetas que tienen código de desarrollo, una con typescript y otra con javascript. De ahí que podamos decir que si hacemos un proyecto UI5 con typescript, también podemos tener entregables con javascript.

Vamos con un ejemplo. Crearemos una carpeta para el proyecto y lo inicializaremos:

mkdir ui5-typescript
cd ui5-typescript
npm init -y
mkdir src

En la carpeta src, crearemos el fichero Component.ts:

import UIComponent from ERROR_BEGIN'sap/ui/core/UIComponent'; //E> Cannot find module 'sap/ui/core/UIComponent' or its corresponding type declarations

/** @namespace ui5.typescript */
export default class Component extends UIComponent {
  init() {
    super.init();
  }
}

Para solucionar el error de la primera línea, instalaremos las dependencias typescript y @types/[email protected]:

npm install --save-dev typescript @types/[email protected]

¡Atención! Es probable que el proyecto en el que estemos trabajando utilice una versión anterior de UI5 respecto a los @types instalados, con lo que los tipos podrían ser incorrectos en algunos casos: nuevas funcionalidades, cambios en la api…

Una vez instaladas las dependencias, yo personalmente opto por quitar los carets que añade npm automáticamente. Así dejamos fijadas las versiones de nuestras dependencias y podremos hacer el upgrade de las versiones cuando nosotros lo decidamos.

{
  "devDependencies": {
    "@types/openui5": "^1.108.0",
    "@types/openui5": "1.108.0",
    "typescript": "^4.9.3",
    "typescript": "4.9.3"
  }
}

Con esto, el error anterior desaparecerá. Ahora ya tenemos un Component en el cual podemos utilizar typescript en cualquier método. Por ejemplo:

import UIComponent from 'sap/ui/core/UIComponent';

/** @namespace ui5.typescript */
export default class Component extends UIComponent {
  init() {
    super.init();
    const suma = this.suma(4, 5);
  }

  suma(a: number, b: number) {
    return a + b;
  }
}

Vemos que la constante suma ya tiene un tipo asociado number. El tipo lo está infiriendo typescript de forma automática por el propio return de la función, ya que se está retornando la suma de dos variables numéricas.

Ahora bien, con esto no es suficiente para que nuestro proyecto funcione al 100%. Tenemos que indicarle al compilador de typescript una serie de configuraciones. Para ello crearemos en la raíz del proyecto el fichero tsconfig.json:

{
  "compilerOptions": {
    "target": "es2015",
    "module": "es2015",
    "moduleResolution": "node",
    "skipLibCheck": true,
    "preserveConstEnums": true,
    "allowJs": true,
    "strict": true,
    "strictNullChecks": false,
    "strictPropertyInitialization": false,
    "rootDir": "./src",
    "outDir": "./dist",
    "baseUrl": "./",
    "paths": {
      "ui5/typescript/*": ["./src/*"]
    }
  },
  "include": ["./src/**/*"]
}

Con esta configuración (extraída directamente de un ejemplo de SAP), tendremos el proyecto listo para programar en typescript. Ahora bien, esto no significa que ya puedas ejecutar el proyecto en el navegador. Faltaría preparar el proyecto para usarlo con ui5 tooling. Habría que instalar el UI5 Tooling con middleware y la task para transpilar.

Como el objetivo de este artículo no es explicar como funciona el tooling, voy a hacer un poco de spam 😇. Casualmente, existe una extensión para vscode llamada ui5-tools, creada por mi mismo 😜. La puedes encontrar en el marketplace de vscode.

Una vez instalada, tendrás soporte para lanzar un servidor local. Lo puedes hacer pulsando sobre el botón de la barra de estado Start UI5 Server | DEV. La extensión se encargará de transpilar los ficheros automáticamente de la carpeta src a la carpeta webapp cada vez que arranques el servidor. Además, según vayas modificándolos, los irá transpilando al vuelo y refrescará automáticamente el navegador. También tiene soporte para sourcemaps, con lo que si todo va bien podrás debugar en el navegador directamente sobre el código typescript.

Algunas cosas a tener en cuenta sobre typescript con ui5:

  • Las funciones invocadas directamente desde la vista, por ejemplo eventos: press, change, etc., reciben unos parámetros genéricos. Deberás castear los tipos para que se correspondan a los tipos reales:
import Controller from 'sap/ui/core/mvc/Controller';
import Event from 'sap/ui/base/Event';
import Input from 'sap/m/Input';
import Context from 'sap/ui/model/Context';
import Button from 'sap/m/Button';
import List from 'sap/m/List';
import StandardListItem from 'sap/m/StandardListItem';

/** @namespace ui5.typescript.controller */
export default class main extends Controller {
  // Evento change de un sap.m.Input
  changeInput(oEvent: Event) {
    const oSource = oEvent.getSource() as Input;
    const oContext = oEvent.getBindingContext() as Context;
    // Este tipo dependerá del contexto
    const oObject = oContext.getObject() as Record<string, any>;
  }

  // Evento press de un sap.m.Button
  pressButton(oEvent: Event) {
    const oSource = oEvent.getSource() as Button;
    const oContext = oSource.getBindingContext() as Context;
    // Este tipo dependerá del contexto
    const oObject = oContext.getObject() as Record<string, any>;
  }

  // Evento itemPress de un sap.m.List
  itemPress(oEvent: Event) {
    const oListItem = oEvent.getParameter('listItem') as StandardListItem;
    const oList = oEvent.getParameter('srcControl') as List;
  }
}
  • Como se puede apreciar en el ejemplo anterior, de primeras no tendremos tipos para todo lo que provenga de un servicio/api. Esos tipos hay diferentes maneras de definirlos: manualmente o automáticamente con algún script o dependencia. Sobre esto no vamos a hablar en este artículo. Para salir del paso se pueden utilizar los Record<string, any> para objetos, o para los array de objetos Record<string, any>[].

  • En los hooks de UI5 como onInit solemos crear cosas en el controller, como modelos, propiedades, etc. Typescript no será capaz de inferir los tipos porque onInit no es una función constructora nativa del lenguaje sino del framework:

import Controller from 'sap/ui/core/mvc/Controller';
import JSONModel from 'sap/ui/model/json/JSONModel';

/** @namespace ui5.typescript.controller */
export default class main extends Controller {
  onInit() {
    this.'ERROR_BEGIN'oModel'ERROR_END' = new JSONModel({}); //E> Property 'oModel' does not exist on type 'Component'
  }
}

Una posible solución, aunque no es lo mismo, sería definirlo como propiedad de clase.

import Controller from 'sap/ui/core/mvc/Controller';
import JSONModel from 'sap/ui/model/json/JSONModel';

/** @namespace ui5.typescript.controller */
export default class main extends Controller {
  oModel = new JSONModel({});

  onInit() {
    this.oModel = new JSONModel({});
  }
}

Una vez transpilado a javascript, el código queda encapsulado en el constructor de la clase. Este código solo se ejecutará al hacer el new Controller(). Si bien se ejecuta en un punto diferente que el hook de ui5 onInit, es bastante probable que para la mayor parte de las veces resulte en el mismo funcionamiento.

Por otro lado, si en el onInit estamos recuperando controles por id o similar, entonces este método no es válido. En mi opinión, lo mejor es crear métodos getter que retornen esos controles. También aplica para el ejemplo anterior:

import Controller from 'sap/ui/core/mvc/Controller';
import JSONModel from 'sap/ui/model/json/JSONModel';
import Table from 'sap/m/Table';

/** @namespace ui5.typescript.controller */
export default class main extends Controller {
  onInit() {
    const oView = this.getView();
    oView.setModel(new JSONModel({}), 'customModel');
    this._table = oView.byId('table');
  }

  getCustomModel() {
    return this.getView().getModel('customModel') as JSONModel;
  }

  getTable() {
    return this.getView().byId('table') as Table;
  }

  async getDataFromService() {
    const oTable = this._table;
    const oTable = this.getTable();

    oTable.setBusy(true);
    try {
      // Lógica para recuperar datos del servicio y volcarlos en oData
      const oData = await Service.getCustomData();
      this.getCustomModel().setData(oData);
    } catch (oError) {
      // Gestión del error
    }
    oTable.setBusy(false);
  }
}

Si lo hacemos así, mantendremos el contexto del controller más limpio, con menos propiedades custom y si problemas para el tipado de typescript.

  • Por último, con ui5 no podemos destructurar un import de las librerías. Hay que añadir una línea por cada módulo que vayamos a importar:
import { Input, List, Button } from 'sap/m';
import Input from 'sap/m/Input';
import List from 'sap/m/List';
import Button from 'sap/m/Button';
import { formatterDecimal, formatterFecha } from 'ui5/typescript/formatter/Formatters'; // ✅

Ahora bien, nosotros si podemos crear ficheros typescript en nuestra app y exportar/importar como consideremos.

Hasta aquí los primeros pasos con typescript. Espero que te haya gustado. ¡Hasta el próximo!