Empezando a trabajar con Typescript

JavaScript es famoso principalmente por tres razones:

  • Es un lenguaje universal: Con el cual podemos trabajar en más sitios (web, app, programas, hardware…).
  • Lenguaje sencillo: Es muy fácil de aprender si nunca has programado.
  • Demasiada simpleza: Quizá que sea tan sencillo ha hecho que se vuelva simple y para un programador que viene de otros lenguajes, no es fácil adaptarse a sus ‘detalles’.

TypeScript es ese superset de JavaScript que nos aporta comodidad al trabajar, gracias al cual programar en JavaScript es cómo programar con un lenguaje orientado a objetos, si no conoces mucho sobre este lenguaje puedes consultar el artículo de introducción en la MSDN Magazine.

 

Buscaminas code kata

Una buena manera de empezar con este nuevo lenguaje es coger un ejemplo de código de internet y resolverlo para obtener el mismo resultado que otro en JavaScript, para este caso he escogido un reto de Solveed donde se propone hacer un buscaminas, con la siguiente solución, que he añadido en un proyecto nuevo de tipo web.

buscaminas

Resolviéndolo con TypeScript

Para entender mejor las diferencias, voy a resolver el mismo reto utilizando TypeScript pero basándome en la solución anterior, para ello voy a abrir en mi Visual Studio Community un nuevo proyecto de TypeScript.

Capture

Una vez tenemos los dos proyectos, ha quedado la siguiente estructura, donde tengo dos proyectos que son el de solveed separado por html css y js y por otro lado la inicialización del de TypeScript que he copiado el html y css del anterior proyecto, añadiendo mi archivo app.ts que al compilarse por primera vez me ha creado un app.js

Además voy a dividir mi espacio de trabajo en dos partes, teniendo a la izquierda app.ts y a la derecha app.js (el original no mi compilación) lo que me va a facilitar el trabajo para crear mi propia lógica del juego.

image explorer

El primer paso va a ser crear una clase con las propiedades mínimas del ejemplo:

    1: // Original
    2: (function() {  
    3:     // variables de configuración 
    4:     var elapsedSeconds = 0; 
    5:     var width = 10, height = 10, minesCount = 10;
    6:     
    7:     window.onload = function() { };
    8: }})(); 

Ha creado una función anónima autoejecutable con varias variables privadas y capturado el evento window.onload

    1: // Typescript
    2: module App {
    3:     export class Game {
    4:         private elapsedSeconds: number;
    5:         private width: number;
    6:         private height: number; 
    7:         private minesCount: number;
    8:  
    9:         constructor(elapsedSeconds: number, width: number, height: number, minesCount: number) {
   10:             this.elapsedSeconds = elapsedSeconds;
   11:             this.width = width;
   12:             this.height = height;
   13:             this.minesCount = minesCount;
   14:         } 
   15: }
   16:  
   17: window.onload = () => {
   18:     var game = new App.Game(0, 10, 10, 10);
   19: };

Creo un módulo en el cual tengo una clase Game que va a ser la clase con todo mi juego, en la cual tengo una serie de propiedades que son números y privadas, todo esto lo inicializo en el mismo evento window.onload

Ahora añadimos los métodos principales:

    1: (function() {  
    2:     // variables de configuración 
    3:     var elapsedSeconds = 0; 
    4:     var width = 10, height = 10, minesCount = 10;
    5:  
    6:     window.onload = function() { 
    7:         setInterval(function() { 
    8:             elapsedSeconds += 1; 
    9:             document.getElementById('timer').innerHTML = elapsedSeconds; 
   10:         }, 1000);
   11:  
   12:         drawMines(width, height, 'mines'); 
   13:         matrix = generateField(width, height, minesCount);                  
   14:         buttons = document.getElementsByName('btn');
   15:     } 
   16: }})(); 
   17:  
   18:  
   19: function generateField(width, height, maxMines) { 
   20:  
   21: } 
   22:  
   23: function drawMines(width, height, elementId) { 
   24:     var div = document.getElementById(elementId); 
   25:     div.innerHTML = ''; 
   26:  
   27:     for (var i = 0; i < height; i++) {
   28:  
   29:         for (var j = 0; j < width; j++) {
   30:             div.innerHTML  += '<input type="button" name="btn" id="' + i + '_' + j + '" style="width:25px; height:25px" value=" "/>'; 
   31:         } 
   32:         div.innerHTML += '<br/>'; 
   33:     } 
   34: } 
   35:  

Seguimos añadiendo las funciones que ha hecho el autor de la solución, no nos vamos a centrar en si es la mejor manera de resolver cada caso, más bien en la estrucutra de los datos.

    1: module App {
    2:     export class Game {
    3:         private elapsedSeconds: number;
    4:         private width: number;
    5:         private height: number; 
    6:         private minesCount: number;
    7:         private interval: number;
    8:         private buttons: any;
    9:  
   10:         constructor(elapsedSeconds: number, width: number, height: number, minesCount: number) {
   11:             this.elapsedSeconds = elapsedSeconds;
   12:             this.width = width;
   13:             this.height = height;
   14:             this.minesCount = minesCount;
   15:  
   16:             this.initializeTimer();
   17:  
   18:             this.drawMines('mines');
   19:             this.generateField();
   20:             this.buttons = document.getElementsByName('btn');
   21:         } 
   22:  
   23:         public initializeTimer() {
   24:             var self = this;
   25:             this.interval = setInterval(() => {
   26:               self.elapsedSeconds++;
   27:               document.getElementById('timer').innerHTML = self.elapsedSeconds.toString();  
   28:             }, 1000);
   29:         }
   30:  
   31:         public drawMines(elementId: string) {
   32:             var div = document.getElementById(elementId);
   33:             div.innerHTML = '';
   34:             for (var i: number = 0; i < this.height; i++) {
   35:                 for (var j: number = 0; j < this.width; j++) {
   36:                     div.innerHTML += '<input type="button" name="btn" id="' + i + '_' + j + '" style="width:25px; height:25px" value=" "/>';
   37:                 }
   38:                 div.innerHTML += '<br/>';
   39:             } 
   40:         }
   41:  
   42:         public generateField() {
   43:            
   44:         }      
   45: }
   46:  
   47: window.onload = () => {
   48:     var game = new App.Game(0, 10, 10, 10);
   49: };

Nuestro código queda mucho mas entendible que de la otra manera. Nótese que en este caso this hace referencia a mi clase.

Generamos generateField y sus métodos privados dependientes:

    1: (function() {  
    2:     // variables de configuración 
    3:     var elapsedSeconds = 0; 
    4:     var width = 10, height = 10, minesCount = 10;
    5:  
    6:     window.onload = function() { 
    7:         setInterval(function() { 
    8:             elapsedSeconds += 1; 
    9:             document.getElementById('timer').innerHTML = elapsedSeconds; 
   10:         }, 1000);
   11:  
   12:         drawMines(width, height, 'mines'); 
   13:         matrix = generateField(width, height, minesCount);                  
   14:         buttons = document.getElementsByName('btn');
   15: }})(); 
   16:  
   17: function generateField(width, height, maxMines) { 
   18:     var field = makeMatrix(width, height); 
   19:     var minesCounter = 0; 
   20:  
   21:     while (minesCounter < maxMines) {
   22:         var randomMine = getRandom(0, 1); 
   23:         var randomPosition = [getRandom(0, width - 1), getRandom(0, height - 1)]; 
   24:  
   25:         if (!field[randomPosition[0]][randomPosition[1]]) {
   26:             minesCounter += (randomMine) ? 1 : 0; 
   27:  
   28:             if (randomMine) {
   29:                 field[randomPosition[0]][randomPosition[1]] =  '*'; 
   30:  
   31:                 for (var x = randomPosition[0] - 1; x <= randomPosition[0] + 1; x++) {
   32:  
   33:                     for (var y=randomPosition[1]-1; y<=randomPosition[1]+1; y++) { 
   34:                         try { 
   35:                             field[x][y] += (field[x][y] != '*') ? 1 : ''; 
 n = +field[x][y];                                     if (field[x][y] != '*') {                                         n += 1;                                         field[x][y] = n.toString();                                     } else {                                         field[x][y] = "";                                     }
   36:                         }catch(e) { }//TypeError probablemente 
   37:                     } 
   38:                 } 
   39:             } 
   40:         } 
   41:     } 
   42:     return field; 
   43: }
   44:  
   45: function getRandom(min, max) {
   46:     return Math.floor(Math.random() * (max - min + 1)); 
   47: } 
   48:  
   49: function makeMatrix(width, height) { 
   50:     var matrix = new Array(height); 
   51:  
   52:     for (var i=0; i<height; i++) { 
   53:         matrix[i] = new Array(width); 
   54:  
   55:         for (var j = 0; j < width; j++) {
   56:             matrix[i][j] = 0; 
   57:         } 
   58:     } 
   59:     return matrix; 
   60: } 

Básicamente estamos generando las celdas de nuestro juego.

    1: module App {
    2:     export class Game {
    3:         private elapsedSeconds: number;
    4:         private width: number;
    5:         private height: number; 
    6:         private minesCount: number;
    7:         private interval: number;
    8:         private buttons: any;
    9:         private matrix: number[][];
   10:  
   11:         constructor(elapsedSeconds: number, width: number, height: number, minesCount: number) {
   12:             this.elapsedSeconds = elapsedSeconds;
   13:             this.width = width;
   14:             this.height = height;
   15:             this.minesCount = minesCount;
   16:  
   17:             this.initializeTimer();
   18:  
   19:             this.drawMines('mines');
   20:             this.generateField();
   21:             this.buttons = document.getElementsByName('btn');
   22:  
   23:             this.initializeGame();
   24:         } 
   25:  
   26:         public initializeTimer() {
   27:             var self = this;
   28:             this.interval = setInterval(() => {
   29:               self.elapsedSeconds++;
   30:               document.getElementById('timer').innerHTML = self.elapsedSeconds.toString();  
   31:             }, 1000);
   32:         }
   33:  
   34:         public drawMines(elementId: string) {
   35:             var div = document.getElementById(elementId);
   36:             div.innerHTML = '';
   37:             for (var i: number = 0; i < this.height; i++) {
   38:                 for (var j: number = 0; j < this.width; j++) {
   39:                     div.innerHTML += '<input type="button" name="btn" id="' + i + '_' + j + '" style="width:25px; height:25px" value=" "/>';
   40:                 }
   41:                 div.innerHTML += '<br/>';
   42:             } 
   43:         }
   44:  
   45:         public generateField() {
   46:             var field: number[][] = this.makeMatrix();
   47:             var minesCounter: number = 0;
   48:             var n: number;
   49:  
   50:             while (minesCounter < this.minesCount) {
   51:                 var randomMine: number = this.getRandom(0, 1);
   52:                 var randomPosition: number[] = [this.getRandom(0, this.width - 1), this.getRandom(0, this.height - 1)];
   53:  
   54:                 if (!field[randomPosition[0]][randomPosition[1]]) {
   55:                     minesCounter += (randomMine) ? 1 : 0;
   56:  
   57:                     if (randomMine) {
   58:                         field[randomPosition[0]][randomPosition[1]] = -10;
   59:  
   60:                         for (var x: number = randomPosition[0] - 1; x <= randomPosition[0] + 1; x++) {
   61:  
   62:                             for (var y: number = randomPosition[1] - 1; y <= randomPosition[1] + 1; y++) {
   63:                                 try {
   64:                                     n = +field[x][y];
   65:                                     if (field[x][y] != -10) {
   66:                                         n += 1;
   67:                                         field[x][y] = n;
   68:                                     }
   69:                                 } catch (e) { }//TypeError probablemente 
   70:                             }
   71:                         }
   72:                     }
   73:                 }
   74:             }
   75:             this.matrix = field;
   76:         }
   77:  
   78:         private makeMatrix() {
   79:             var matrix: number[][];
   80:             matrix = new Array(this.height);
   81:  
   82:             for (var i: number = 0; i < this.height; i++) {
   83:                 matrix[i] = new Array(this.width);
   84:  
   85:                 for (var j: number = 0; j < this.width; j++) {
   86:                     matrix[i][j] = 0;
   87:                 }
   88:             }
   89:             return matrix;
   90:         }
   91:  
   92:         private getRandom(min: number, max: number) {
   93:             return Math.floor(Math.random() * (max - min + 1));
   94:         } 
   95: }
   96:  
   97: window.onload = () => {
   98:     var game = new App.Game(0, 10, 10, 10);
   99: };

En esta parte vemos lo fácil que es tener métodos privados.

Por último inicializamos las celdas y comprobamos los movimientos

    1: (function() {  
    2:     window.onload = function() { 
    3:         for (var i = 0; i < buttons.length; i++) { 
    4:             document.getElementById(buttons[i].id).onclick = function(e) { 
    5:             var point = e.target.id.split('_'); 
    6:             var value = matrix[parseInt(point[0])][parseInt(point[1])]; 
    7:  
    8:             if (matrix[parseInt(point[0])][parseInt(point[1])] == '*') {
    9:                 e.target.value = '*'; 
   10:                 alert('You lose!'); 
   11:                 location.href = location.href;
   12:             } else { 
   13:                 expand(parseInt(point[0]), parseInt(point[1]), matrix);                 
   14:             } 
   15:  
   16:             if (isWinner(width, height, minesCount)) { 
   17:                 alert('You win!'); 
   18:                 location.href = location.href;
   19:             } 
   20:         } 
   21:     } 
   22: }})(); 
   23:   
   24: function isWinner(width, height, minesNumber) {  
   25:     var enabledCounter = 0; 
   26:  
   27:     for (var y = 0; y < height; y++) {
   28:  
   29:         for (var x=0; x<width; x++) { 
   30:             btn = document.getElementById(x + '_' + y);
   31:  
   32:             if (!btn.disabled) {
   33:                 enabledCounter++;
   34:             }
   35:         } 
   36:     }    
   37:     return enabledCounter == minesNumber; 
   38: } 
   39:  
   40: function expand(x, y, matrix) { 
   41:     btn = document.getElementById(x + '_' + y); 
   42:     var value = matrix[x][y]; 
   43:  
   44:     if (btn.disabled) {
   45:         return false;
   46:     }
   47:  
   48:     if (value && value != '*') { 
   49:         btn.value = value; 
   50:         btn.disabled = 'true'; 
   51:         return false; 
   52:     } 
   53:  
   54:     if (!value) { 
   55:         btn.value = ' '; 
   56:         btn.disabled = 'true'; 
   57:     } 
   58:  
   59:     var limits = [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1], [x - 1, y - 1], [x + 1, y - 1], [x - 1, y + 1], [x + 1, y + 1]];
   60:  
   61:     for (var i=0; i<limits.length; i++) { 
   62:         try { 
   63:  
   64:             if (matrix[limits[i][0]][limits[i][1]] != '*') {  
   65:                 expand(limits[i][0], limits[i][1], matrix);  
   66:             } 
   67:         } catch(e) { } 
   68:     } 
   69: } 
   70:  

Este código se llama en la funcion principal.

    1: private initializeGame() {
    2:             var self = this;
    3:             for (var i: number = 0; i < this.buttons.length; i++) {
    4:                 document.getElementById(this.buttons[i].id).onclick = (e: any) => {
    5:                     var point = e.target.id.split('_');
    6:                     var value = self.matrix[parseInt(point[0])][parseInt(point[1])];
    7:  
    8:                     if (self.matrix[parseInt(point[0])][parseInt(point[1])] == -10) {
    9:                         e.target.value = '*';
   10:                         alert('You lose!');
   11:                         location.href = location.href;
   12:                     } else {
   13:                         self.expand(parseInt(point[0]), parseInt(point[1]), this.matrix);
   14:                     }
   15:  
   16:                     if (self.isWinner(this.width, this.height, this.minesCount)) {
   17:                         alert('You win!');
   18:                         location.href = location.href;
   19:                     }
   20:                 }
   21:             }
   22:         }
   23:  
   24:         public isWinner(width: number, height: number, minesNumber: number) {
   25:             var enabledCounter = 0;
   26:             var btn: any;
   27:  
   28:             for (var y = 0; y < height; y++) {
   29:  
   30:                 for (var x = 0; x < width; x++) {
   31:                     btn = document.getElementById(x + '_' + y);
   32:  
   33:                     if (!btn.disabled) {
   34:                         enabledCounter++;
   35:                     }
   36:                 }
   37:             }
   38:             return enabledCounter == minesNumber;
   39:         }
   40:  
   41:         private expand(x: number, y: number, matrix: any) {
   42:             var btn: any;
   43:             btn = document.getElementById(x + '_' + y);
   44:             var value = matrix[x][y];
   45:  
   46:             if (btn.disabled) {
   47:                 return false;
   48:             }
   49:  
   50:             if (value && value != '*') {
   51:                 btn.value = value;
   52:                 btn.disabled = 'true';
   53:                 return false;
   54:             }
   55:  
   56:             if (!value) {
   57:                 btn.value = ' ';
   58:                 btn.disabled = 'true';
   59:             }
   60:  
   61:             var limits = [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1], [x - 1, y - 1], [x + 1, y - 1], [x - 1, y + 1], [x + 1, y + 1]];
   62:  
   63:             for (var i: number = 0; i < limits.length; i++) {
   64:                 try {
   65:  
   66:                     if (matrix[limits[i][0]][limits[i][1]] != '*') {
   67:                         this.expand(limits[i][0], limits[i][1], matrix);
   68:                     }
   69:                 } catch (e) { }
   70:             }
   71:         } 
   72:     }

Que por supuesto podemos encapsular en un método privado al que he llamado initializeGame.

Conclusiones

Después de trabajar un poco con TypeScript es posible que no quieras volver atrás, es verdad que en muchas ocasiones hay que escribir más código, pero lo hace más correcto y fácil de modificar o incluso escalar.

Tienes el código completo y actualizado subido a nuestro github para poder consultarlo o incluso comentarlo.

_____________________________

Quique Fernández

Technical Evangelist Intern

@CKGrafico