Tipos de datos estáticos
JavaScript es un lenguaje de programación tipado dinámicamente, es decir,
es el valor que se asigna a una variable el que permite inferir dinámicamente
el tipo de dicha variable. En el siguiente ejemplo JavaScript, que ilustra lo
anterior, la variable myVar cambia su tipo de datos dependiendo del tipo del
valor que se le asigna:
let myVar;
console.log(`${myVar} = ${typeof myVar}`);
myVar = 12;
console.log(`${myVar} = ${typeof myVar}`);
myVar = "Hello";
console.log(`${myVar} = ${typeof myVar}`);
myVar = true;
console.log(`${myVar} = ${typeof myVar}`);
Cabe mencionar la primera asignación, donde la variable myVar no ha recibido ningún
valor y, por lo tanto, su tipo no se puede inferir dinámicamente. Es por ello que es
de tipo undefined y su valor es undefined también.
A modo de recordatorio, JavaScript cuenta con los siguientes tipos de datos:
number: representa valores numéricos.string: representa cadenas de caracteres.boolean: toma los valores lógicostrueyfalse.symbol: representa valores constantes únicos.null: solo puede tomar el valornulle indica una referencia que no existe o no válida.undefined: este tipo (y valor) es el que toma una variable que se ha definido pero a la que no se ha asignado valor alguno.object: representa valores complejos, formados por pares propiedad–valor.
Tal y como se ha ilustrado en la sección de justificación del uso de TypeScript, el tipado dinámico puede llevar a la aparición de problemas. TypeScript, que es un lenguaje con tipos de datos estáticos, requiere de la especificación explícita de los tipos de datos, lo que permite al compilador detectar posibles errores cuando se intentan utilizar tipos de datos diferentes a los esperados. Los tipos de datos que pueden explicitarse en TypeScript son los mismos que en JavaScript, como cabría esperar, dado que TypeScript es un superconjunto de JavaScript.
Anotaciones de tipo e inferencia de tipos
En TypeScript, los tipos de datos estáticos se definen explícitamente utilizando anotaciones de tipo:
function add(firstNum: number, secondNum: number): number {
return firstNum + secondNum;
}
const myConst: number = 7;
let myResult: number = add(1, myConst);
console.log(`myResult = ${myResult}`);
En el ejemplo anterior, se ha utilizado una anotación de tipo para los parámetros de la función
add, así como una anotación de tipo para el resultado que devuelve la misma. También se puede
observar como se pueden llevar a cabo anotaciones de tipo en constantes y variables.
A pesar de que se pueden indicar los tipos de datos estáticos de manera explícita, el compilador de TypeScript también es capaz de inferir los tipos de datos de manera implícita:
function add(firstNum: number, secondNum: number) {
return firstNum + secondNum;
}
const myConst = 7;
let myResult = add(1, myConst);
console.log(`myResult = ${myResult}`);
Se puede observar como no hemos indicado de manera explícita los tipos de la
constante myConst y la variable myResult. Tampoco se ha explicitado el tipo
que retorna la función add. Dado que los tipos se utilizan correctamente,
el compilador no detecta ningún tipo de error.
Sin embargo, si se modifica, por ejemplo, la definición de la función add:
function add(firstNum: string, secondNum: string) {
return firstNum + secondNum;
}
const myConst = 7;
let myResult = add(1, myConst);
console.log(`myResult = ${myResult}`);
El compilador infiere que la constante myConst es de tipo number debido a la asignación
del valor numérico 7. Más tarde, detectará que se está intentando invocar a la función add
con un argumento de tipo number, a pesar de haberla declarado con parámetros de tipo string,
lo cual hará que informe de un error:
src/index.ts:6:20 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
6 let myResult = add(1, myConst);
~
Found 1 error in src/index.ts:6
El compilador de TypeScript cuenta con una opción interesante que, si se habilita, permite conocer
los tipos de datos que se están utilizando en el código. Dicha opción es declaration:
{
"compilerOptions": {
"target": "es2024",
"module": "commonjs",
"rootDir": "./src",
"declaration": true,
"outDir": "./dist"
}
}
Si volvemos a recompilar, observando el contenido del directorio /dist, ya no solo existe un fichero
index.js, sino también un fichero index.d.ts, el cual contiene información sobre los tipos de datos,
ya se hayan definido explícitamente mediante anotaciones de tipo o hayan sido inferidos por el compilador.
declare function add(firstNum: string, secondNum: string): string;
declare const myConst = 7;
declare let myResult: string;
El tipo de datos any
El tipo de datos any permite indicar que, por ejemplo, una constante o variable, un parámetro
de una función o un resultado devuelto por la misma sea de cualquier tipo. En el siguiente ejemplo,
el compilador de TypeScript informará de un error, dado que el resultado de la función add, que
es de tipo number, se está intentando asignar a la variable myResult, que es de tipo string.
function add(firstNum: number, secondNum: number) {
return firstNum + secondNum;
}
const myConst = 7;
let myResult: string = add(1, myConst);
console.log(`myResult = ${myResult}`);
Para evitar lo anterior, podemos indicar, explícitamente, que la función add devuelve un valor
de tipo any:
function add(firstNum: number, secondNum: number): any {
return firstNum + secondNum;
}
const myConst = 7;
let myResult: string = add(1, myConst);
console.log(`myResult = ${myResult}`);
Utilizar el tipo de datos any permite a TypeScript acceder a todas las funcionalidades proporcionadas
por el tipado dinámico de JavaScript. No obstante, su uso también suele implicar los mismos errores,
muchas veces inesperados, que surgen cuando programamos directamente en JavaScript, los cuales suelen
aparecer en tiempo de ejecución debido a la transformación automática de unos tipos de datos en otros.
El siguiente ejemplo ilustra lo anterior:
function add(firstNum: number, secondNum: number): any {
return `Result is ${firstNum + secondNum}`;
}
let myResult = add(7, 8) / 2;
console.log(`myResult = ${myResult}`);
La función add devuelve un valor de tipo any. En tiempo de ejecución, el valor devuelto es de tipo
string y consiste en una cadena de caracteres que comienza con “Result is …“. Al intentar dividir
el resultado devuelto por la función add, el mismo no puede transformarse a un valor numérico, por lo
que la variable myResult, tras llevar a cabo la división, toma el valor NaN o Not a Number.
Otro ejemplo que ilustra este tipo de problemas podría ser el siguiente:
function add(firstNum: number, secondNum: number): any {
return `Result is ${firstNum + secondNum}`;
}
let myResult: number = add(7, 8);
console.log(`myResult = ${myResult.toFixed(2)}`);
El resultado de la función add, que es de tipo any, puede asignarse a la variable myResult, que es
de tipo number, y el compilador de TypeScript no emite ningún tipo de aviso al respecto, tal y como se
esperaba. En tiempo de ejecución, no obstante, la función add devuelve un valor de tipo string que se
asigna a la variable myResult y, debido al tipado dinámico de JavaScript, en ese justo momento,
myResult pasa a ser de tipo string. La invocación del método toFixed implica un error en tiempo de
ejecución, dado que el método toFixed no se encuentra disponible para ser invocado al pertenecer al tipo
number y no al tipo string:
[~/DSI/theory-examples()]$tsc
[~/DSI/theory-examples()]$node dist/index.js
/home/usuario/DSI/theory-examples/dist/index.js:7
console.log(`myResult = ${myResult.toFixed(2)}`);
^
TypeError: myResult.toFixed is not a function
at Object.<anonymous> (/home/usuario/DSI/theory-examples/dist/index.js:7:36)
at Module._compile (node:internal/modules/cjs/loader:1760:14)
at Object..js (node:internal/modules/cjs/loader:1892:10)
at Module.load (node:internal/modules/cjs/loader:1480:32)
at Module._load (node:internal/modules/cjs/loader:1299:12)
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:245:24)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47
Node.js v25.3.0
Además de usar anotaciones de tipo explícitas para utilizar el tipo de datos any, TypeScript asignará
el tipo de datos any de manera implícita siempre y cuando no sea capaz de inferir un tipo de datos
más concreto. Lo anterior permite aplicar TypeScript en proyectos JavaScript de manera selectiva. También
permite simplificar el trabajo con paquetes de terceros desarrollados en JavaScript.
function add(firstNum, secondNum) {
return firstNum + secondNum;
}
let myResult = add(1, 7);
console.log(`myResult = ${myResult}`);
myResult = add('1', '7');
console.log(`myResult = ${myResult}`);
myResult = add(true, true);
console.log(`myResult = ${myResult}`);
En el ejemplo anterior, el compilador no es capaz de inferir un tipo de datos específico para los
parámetros de la función add y, tampoco, para su resultado, por lo que infiere que todos son de
tipo any. El tipo de datos inferido para la variable myResult también será any. De hecho,
se trata del código que escribiríamos directamente en JavaScript. El compilador de TypeScript, por
defecto, no indicará ningún tipo de error y, además, el código generado se ejecutará correctamente,
llevando a cabo, por supuesto, las correspondientes transformaciones de un tipo de datos a otro.
Utilizar el tipo de datos any de un modo explícito cuidadosamente podría llegar a beneficiarnos en
algunos casos. No obstante, permitir que el compilador de TypeScript infiera el tipo de datos any
de manera implícita implica la aparición de errores relacionados con los tipos de datos,
como ya ocurre con JavaScript. Es una buena práctica, por tanto, desactivar el uso implícito del
tipo de datos any habilitando la opción noImplicitAny del compilador de TypeScript. Al activar
dicha opción en el compilador, la siguiente vez que tratemos de compilar el último ejemplo de código,
obtendremos lo siguiente:
src/index.ts:1:14 - error TS7006: Parameter 'firstNum' implicitly has an 'any' type.
1 function add(firstNum, secondNum) {
~~~~~~~~
src/index.ts:1:24 - error TS7006: Parameter 'secondNum' implicitly has an 'any' type.
1 function add(firstNum, secondNum) {
~~~~~~~~~
Found 2 errors in the same file, starting at: src/index.ts:1
De este modo, será necesario especificar un tipo de datos en los parámetros de la función add,
el cual podría ser, de manera intencionada, any. Si por ejemplo los especificamos como number:
function add(firstNum: number, secondNum: number) {
return firstNum + secondNum;
}
let myResult = add(1, 7);
console.log(`myResult = ${myResult}`);
myResult = add('1', '7');
console.log(`myResult = ${myResult}`);
myResult = add(true, true);
console.log(`myResult = ${myResult}`);
Compilando el anterior código, ahora obtendremos lo siguiente desde el compilador:
src/index.ts:7:16 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
7 myResult = add('1', '7');
~~~
src/index.ts:9:16 - error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'number'.
9 myResult = add(true, true);
~~~~
Found 2 errors in the same file, starting at: src/index.ts:7
En este caso, la segunda y tercera invocaciones a la función add dejarían de ser válidas, dado que no
estamos utilizando argumentos con el mismo tipo de datos especificado en la declaración de la función.
Uniones de tipos
Teniendo en cuenta los posibles extremos en las anotaciones de tipos, por un lado, tenemos el tipo de datos
any, lo que permite una total libertad. En el otro extremo se encontraría un tipo de datos específico como,
por ejemplo, boolean, number, e incluso, un tipo literal, los cuales veremos más adelante. Entre ambos extremos
se encuentran las uniones de tipos o type unions, que permiten especificar un conjunto de tipos de datos. El
siguiente ejemplo ilustra una unión de los tipos number y string para el resultado de la función add:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, true);
console.log(`myResult = ${myResult}`);
myResult = add(1, 7, false);
console.log(`myResult = ${myResult}`);
En este punto, es importante indicar que las propiedades de una unión de tipos son aquellas incluidas en
la intersección de las propiedades de los tipos que componen la unión. En el ejemplo anterior, el compilador
infiere que la variable myResult es de tipo number | string (unión de tipos) y no number o string.
Para ilustrar lo anterior, ejecute el siguiente código:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, true);
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
myResult = add(1, 7, false);
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
El resultado de la ejecución será algo como lo siguiente:
src/index.ts:8:22 - error TS2339: Property 'toFixed' does not exist on type 'string | number'.
Property 'toFixed' does not exist on type 'string'.
8 console.log(myResult.toFixed(2));
~~~~~~~
src/index.ts:11:22 - error TS2339: Property 'charAt' does not exist on type 'string | number'.
Property 'charAt' does not exist on type 'number'.
11 console.log(myResult.charAt(0));
~~~~~~
Found 2 errors in the same file, starting at: src/index.ts:8
La propiedad (método) toFixed pertenece al tipo de datos number, mientras que la propiedad
(método) charAt pertenece al tipo string. Sin embargo, ninguna de esas propiedades pertenece
a la unión de tipos number | string, lo que se traduce en que el compilador informe de los
errores expuestos más arriba.
Tipos literales
Se pueden utilizar literales como, por ejemplo, cadenas de caracteres o constantes numéricas, para
llevar a cabo anotaciones de tipo explícitas. Retomando el ejemplo del apartado anterior, podríamos tener
la siguiente alternativa al uso del parámetro isNumber de tipo boolean:
function add(firstNum: number, secondNum: number,
conversionMode: string): number | string {
if (conversionMode === "asNumber") {
return firstNum + secondNum;
}
return (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, "asNumber");
console.log(`myResult = ${myResult}`);
myResult = add(1, 7, "asString");
console.log(`myResult = ${myResult}`);
myResult = add(1, 7, "asWhatever");
console.log(`myResult = ${myResult}`);
Se puede observar como ahora hemos definido el parámetro conversionMode que permite indicar en un string
si el resultado de la operación debe devolverse como un number o como un string. Existen dos problemas
con el código anterior:
- Debo recordar que el parámetro
conversionModedebe recibir como argumento la cadena"asNumber"para obtener un resultado de tiponumber. - Puedo utilizar cualquier cadena de caracteres diferente a
"asNumber"para obtener un resultado de tipostring.
Los anteriores problemas se pueden solucionar utilizando una anotación de tipo literal en la definición del
parámetro conversionMode de la función:
function add(firstNum: number, secondNum: number,
conversionMode: "asNumber" | "asString"): number | string {
if (conversionMode === "asNumber") {
return firstNum + secondNum;
}
return (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, "asNumber");
console.log(`myResult = ${myResult}`);
myResult = add(1, 7, "asString");
console.log(`myResult = ${myResult}`);
myResult = add(1, 7, "asWhatever");
console.log(`myResult = ${myResult}`);
De hecho, ahora la última invocación a add en el ejemplo anterior hará que el compilador de TypeScript
informe del siguiente error:
src/index.ts:13:22 - error TS2345: Argument of type '"asWhatever"' is not assignable to parameter of type '"asNumber" | "asString"'.
13 myResult = add(1, 7, "asWhatever");
~~~~~~~~~~~~
Found 1 error in src/index.ts:13
Ahora, solo podremos invocar la función add con un tercer argumento que sea, o bien
la cadena "asNumber", o bien la cadena "asString".
Por último, cabe mencionar que los tipos literales suelen combinarse con uniones de tipo como las vistas en los últimos ejemplos, aunque también pueden utilizarse de manera independiente. Por ejemplo, se usa un tipo literal, generalmente inferido por el compilador de TypeScript, cuando se declara una constante y se le asigna un valor literal:
const myNumericConst = 7.8;
const myStringConst = "Eduardo";
const myBooleanConst = true;
Alias de tipos y tipos personalizados
En TypeScript podemos definir alias para los tipos de datos y, como consecuencia de lo anterior, podemos definir nuestros propios tipos. Esta funcionalidad permite que el código que desarrollemos sea mucho más limpio y legible, siempre y cuando utilicemos alias suficientemente descriptivos.
Siguiendo con el ejemplo del apartado anterior, podríamos definir un tipo personalizado para la unión de tipos
literales del parámetro conversionMode de la función add, así como para el resultado que devuelve:
type ConversionType = "asNumber" | "asString";
type ResultType = number | string;
function add(firstNum: number, secondNum: number,
conversionMode: ConversionType): ResultType {
if (conversionMode === "asNumber") {
return firstNum + secondNum;
}
return (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, "asNumber");
console.log(`myResult = ${myResult}`);
myResult = add(1, 7, "asString");
console.log(`myResult = ${myResult}`);
Se puede observar como hemos definido dos nuevos tipos a través de los alias ConversionType y ResultType. El primero
de ellos consiste en una unión de tipos que hace uso de tipos literales, mientras que el segundo representa la unión de
los tipos string y number. Nótese que es una buena práctica definir los alias de los nuevos tipos con una letra mayúscula.
Una vez definido un nuevo tipo, el mismo puede utilizarse como cualquier otro tipo en el resto de nuestro código.
Por último, comentar que los alias se pueden utilizar para definir tipos personalizados más complejos. A lo largo de la asignatura, veremos numerosos ejemplos relacionados con lo anterior.
Afirmaciones de tipos
Una afirmación de tipo o type assertion permite indicarle al compilador de TypeScript que trate a un valor como de un tipo de datos concreto. Esto se conoce como type narrowing.
Una afirmación de tipo es una de las maneras de aplicar type narrowing a una unión de tipos:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myNumResult = add(1, 7, true) as number;
console.log(`myNumResult = ${myNumResult}`);
console.log(myNumResult.toFixed(2));
let myStrResult = add(1, 7, false) as string;
console.log(`myStrResult = ${myStrResult}`);
console.log(myStrResult.charAt(0));
Hemos especificado que el compilador debe tratar el valor asignado a la variable myNumResult
como un number, mientras que debe tratar el valor asignado a la variable myStrResult como
un string. Además, el compilador infiere, implícitamente, el tipo de la variable a través
del tipo utilizado para la afirmación. El siguiente ejemplo es equivalente al anterior:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myNumResult: number = add(1, 7, true) as number;
console.log(`myNumResult = ${myNumResult}`);
console.log(myNumResult.toFixed(2));
let myStrResult: string = add(1, 7, false) as string;
console.log(`myStrResult = ${myStrResult}`);
console.log(myStrResult.charAt(0));
Dado que una afirmación de tipo selecciona uno de los tipos de la unión, ahora, en tiempo
de compilación, todos los métodos y propiedades de dicho tipo están disponibles para ser
utilizados, evitando los errores que el compilador informaba. Es por ello que, ahora, si
podemos invocar los métodos toFixed y charAt.
¿Qué sucedería si tratásemos de utilizar una afirmación de tipo no esperada?
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myNumResult = add(1, 7, true) as number;
console.log(`myNumResult = ${myNumResult}`);
console.log(myNumResult.toFixed(2));
let myStrResult = add(1, 7, false) as string;
console.log(`myStrResult = ${myStrResult}`);
console.log(myStrResult.charAt(0));
let myBoolResult = add(1, 7, false) as boolean;
console.log(`myBoolResult = ${myBoolResult}`);
Obtenemos el siguiente error:
src/index.ts:12:20 - error TS2352: Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type 'number' is not comparable to type 'boolean'.
12 let myBoolResult = add(1, 7, false) as boolean;
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Found 1 error in src/index.ts:12
Para solucionar lo anterior, lo que suele hacerse es:
- Utilizar un tipo de datos diferente en la afirmación de tipos que no arroje un error por el compilador.
- Ampliar la unión de tipos con un tipo de datos nuevo.
- Forzar la afirmación de tipos, utilizando primero una afirmación al tipo
unknowno al tipoanye, inmediatamente, al tipo deseado.
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myNumResult = add(1, 7, true) as number;
console.log(`myNumResult = ${myNumResult}`);
console.log(myNumResult.toFixed(2));
let myStrResult = add(1, 7, false) as string;
console.log(`myStrResult = ${myStrResult}`);
console.log(myStrResult.charAt(0));
let myBoolResult = add(1, 7, false) as any as boolean;
console.log(`myBoolResult = ${myBoolResult}`);
myBoolResult ? console.log("I am a true value") :
console.log("I am a false value");
Lo anterior, en tiempo de compilación, permite evitar que el compilador informe del error que obtuvimos más arriba.
No obstante y, siempre que utilizamos el tipo any, como ya hemos visto anteriormente, debemos asegurarnos de que,
en tiempo de ejecución, no vaya a producirse ningún error. En tiempo de ejecución, la función add devolverá un
string que será asignado a la variable myBoolResult, la cual pasará a ser de tipo string (tipado dinámico
de JavaScript). A la hora de utilizar un string en una sentencia condicional, solo la cadena vacía se evalúa como
falsa en JavaScript, mientras que una cadena como la que contiene myBoolResult se evaluará como verdadera.
Es por ello que en la terminal se mostrará el mensaje "I am a true value".
Guardianes de tipo
Un guardián de tipo o type guard implica el uso de typeof para averiguar un tipo de datos, que tiene que ser
uno de los tipos básicos de JavaScript. Es una alternativa a la utilización de afirmaciones de tipo:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, true);
if (typeof myResult === "number") {
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
} else if (typeof myResult === "string") {
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
}
En tiempo de ejecución, myResult recibirá un valor de tipo number, y dicha variable pasará a ser
de ese tipo también. El guardián de tipo establecido a través del uso de typeof permitirá bifurcar
el flujo de ejecución correctamente. Además, el compilador de TypeScript no informará de ningún error
dado que confía en que las sentencias de un bloque condicional concreto solo se ejecutarán si el valor
comprobado es del tipo especificado en la condición. Es por ello que también podemos invocar al método
toFixed dentro del primer bloque condicional y al método charAt dentro del segundo bloque condicional.
El compilador de TypeScript también es capaz de detectar guardianes de tipo en sentencias switch y no
solo en sentencias if-else:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, true);
switch (typeof myResult) {
case "number":
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
break;
case "string":
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
break;
}
El tipo de datos never
TypeScript proporciona el tipo de datos never para aquellos casos en los que un guardián de tipos
ya ha comprobado todos los posibles tipos de datos de un valor concreto. Una vez ha hecho todas
esas comprobaciones, solo permitirá asignar el tipo never a un valor.
En el ejemplo que hemos estado desarrollando, la unión de tipos solo permite los tipos number o
string y la sentencia switch que utilizamos en el último ejemplo ya se encargó de llevar a cabo
las comprobaciones con ambos tipos:
function add(firstNum: number, secondNum: number,
isNumber: boolean): number | string {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult = add(1, 7, true);
switch (typeof myResult) {
case "number":
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
break;
case "string":
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
break;
default:
let result = myResult;
console.log(`Type was not expected: ${result}`);
}
Alcanzar el caso por defecto de la sentencia switch indicaría que ha ocurrido algún tipo de
error durante la ejecución. TypeScript proporciona el tipo de datos never para evitar
utilizar un valor una vez que se ha utilizado un guardián para comprobar todos sus posibles
tipos de datos. Puede comprobar como el compilador de TypeScript ha inferido el tipo de datos
never para la variable result declarada en el bloque default de la sentencia switch.
El tipo de datos unknown
Ya hemos visto anteriormente que el tipo de datos any permite acceder a la flexibilidad que
proporciona JavaScript respecto al tipado de datos dinámico, lo cual también podría llevarnos
a cometer ciertos errores.
El uso del tipo de datos unknown es una alternativa más segura al uso de any.
Un valor unknown solo puede asignarse al tipo de datos any o unknown, a no ser que
se utilice una afirmación o un guardián de tipo, lo que introduce un nivel de
comprobación adicional respecto a la utilización de any:
function add(firstNum: number, secondNum: number,
isNumber: boolean): unknown {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult: number = add(1, 7, true);
console.log(myResult);
El anterior ejemplo, donde se ha indicado que el tipo devuelto por la función add es unknown,
hace que el compilador de TypeScript informe del siguiente error cuando el resultado de la invocación
a la función add se intenta asignar a una variable de tipo number, algo que no ocurriría si se
hubiera indicado any como el tipo devuelto por la función:
rc/index.ts:6:5 - error TS2322: Type 'unknown' is not assignable to type 'number'.
6 let myResult: number = add(1, 7, true);
~~~~~~~~
Found 1 error in src/index.ts:6
El error anterior puede solucionarse, por ejemplo, haciendo uso de una afirmación de tipo
aplicada sobre la variable myResult:
function add(firstNum: number, secondNum: number,
isNumber: boolean): unknown {
return isNumber ? firstNum + secondNum : (firstNum + secondNum).toFixed(2);
}
let myResult: number = add(1, 7, true) as number;
console.log(myResult);
Tipos de datos null y undefined
Tal y como se ha mencionado con anterioridad, el valor null representa algo que no existe o
que no es válido. Al mismo tiempo, el valor undefined se usa cuando se ha definido una variable
a la que no se ha asignado un valor aún.
El compilador de TypeScript, por defecto, permite que los valores null y undefined puedan asignarse
a todos los tipos:
function div(
numerator: number,
denominator: number,
isNumber: boolean,
): number | string {
if (denominator === 0) {
return null;
}
return isNumber
? numerator / denominator
: (numerator / denominator).toFixed(2);
}
let myResult: number | string = div(1, 0, true);
switch (typeof myResult) {
case "number":
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
break;
case "string":
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
break;
default:
let result = myResult;
console.log(`Type was not expected: ${result}`);
}
En el ejemplo anterior se ha definido una función div que devuelve null en caso de que
se proporcione un denominador igual a cero. Tal y como puede observarse, el compilador de TypeScript
no informa de ningún error, a pesar de que se ha indicado explícitamente que el resultado de
dicha función devuelve una unión de tipos number | string. Al mismo tiempo, tampoco informa
de que a la variable myResult, que también se ha anotado explícitamente con el tipo de dicha
unión, se le puede asignar el valor null, resultado de una división por cero.
En tiempo de ejecución, no obstante, el valor devuelto es null, lo que hará que la variable
myResult tome el tipo de datos null. Más tarde, en la sentencia switch, se alcanzará el
caso por defecto:
Type was not expected: null
Para evitar situaciones como las anteriores, el compilador de TypeScript proporciona la opción
strictNullChecks. Si se habilita dicha opción, el compilador no permitirá la asignación de
los valores null y undefined a otros tipos que no sean los correspondientes. Al habilitar
dicha opción en el fichero tsconfig.json e intentar recompilar el ejemplo anterior,
el compilador informará del siguiente error:
src/index.ts:7:5 - error TS2322: Type 'null' is not assignable to type 'string | number'.
7 return null;
~~~~~~
Found 1 error in src/index.ts:7
Para solucionarlo, podríamos modificar el cuerpo de la función para evitar que la misma devuelva el
valor null, o podemos ampliar la unión de tipos utilizada para anotar el tipo del resultado de la
función div, indicando que el tipo null también forma parte de dicha unión. Además, en este último
caso, no solo se debe modificar la anotación en la función, sino también en la declaración de la variable
myResult:
function div(
numerator: number,
denominator: number,
isNumber: boolean,
): number | string | null {
if (denominator === 0) {
return null;
}
return isNumber
? numerator / denominator
: (numerator / denominator).toFixed(2);
}
let myResult: number | string | null = div(1, 0, true);
switch (typeof myResult) {
case "number":
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
break;
case "string":
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
break;
default:
let result = myResult;
console.log(`Type was not expected: ${result}`);
}
Debemos tener en cuenta también la posibilidad de obtener un valor null en
el guardián de tipos. La pregunta que podrían hacerse ahora mismo es ¿por qué no utilizar otro
case en dicho guardián para comprobar si myResult es de tipo null. Modifiquemos el ejemplo anterior
para incluir lo siguiente:
let myResult: number | string | null = div(1, 0, true);
console.log(`${typeof myResult}`)
Al aplicar typeof al tipo null se obtiene como respuesta object. Es por ello que no se puede utilizar
otra sentencia case dentro del switch que haga referencia al tipo de datos null. Lo que se debe hacer
es comprobar explícitamente si el valor obtenido de la ejecución de la función div es null dentro de una
sentencia if-else, la cual es tratada como un guardián de tipos por el compilador de TypeScript:
function div(
numerator: number,
denominator: number,
isNumber: boolean,
): number | string | null {
if (denominator === 0) {
return null;
}
return isNumber
? numerator / denominator
: (numerator / denominator).toFixed(2);
}
let myResult: number | string | null = div(1, 0, true);
switch (typeof myResult) {
case "number":
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
break;
case "string":
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
break;
default:
if (myResult === null) {
console.log(`myResult = ${myResult}`);
} else {
let result = myResult;
console.log(`Type was not expected: ${result}`);
}
}
Cabe mencionar en este punto, no obstante, que, al aplicar typeof al tipo undefined, si que se obtiene
como respuesta "undefined".
Los tipos null y undefined no cuentan con ningún método o propiedad, por lo que si los incluimos en una
unión de tipos, la intersección de propiedades y métodos de los tipos de la unión se encontrará vacía, y no
podremos utilizar métodos o propiedades, al menos, directamente, de aquellos valores cuyo tipo sea esa unión.
Para evitar lo anterior podremos extraer el tipo null de la unión con una afirmación de tipo no nula,
consistente en utilizar el carácter ! después del valor para el cual, bajo nuestra responsabilidad,
afirmaremos que no tomará el valor null:
function div(numerator: number, denominator: number,
isNumber: boolean): number | string | null {
if (denominator === 0) {
return null;
}
return isNumber ? numerator / denominator :
(numerator / denominator).toFixed(2);
}
let myResult: number | string = div(1, 2, true)!;
switch (typeof myResult) {
case "number":
console.log(`myResult = ${myResult}`);
console.log(myResult.toFixed(2));
break
;
case "string":
console.log(`myResult = ${myResult}`);
console.log(myResult.charAt(0));
break;
default:
if (myResult === null) {
console.log(`myResult = ${myResult}`);
} else {
let result = myResult;
console.log(`Type was not expected: ${result}`);
}
}
Se puede observar como a la variable myResult, que es de tipo number | string, se le puede
asignar el resultado devuelto por la función div, a pesar de que la misma devuelve un valor
de tipo number | string | null. Lo anterior se permite escribiendo el carácter ! al final
de la invocación a la función div, indicando que estamos seguros de que esa invocación va a
devolver un valor no nulo.
Un método alternativo a usar una afirmación no nula es utilizar un guardián de tipos, tal y como se ha visto anteriormente:
function div(
numerator: number,
denominator: number,
isNumber: boolean,
): number | string | null {
if (denominator === 0) {
return null;
}
return isNumber
? numerator / denominator
: (numerator / denominator).toFixed(2);
}
let myResult: number | string | null = div(1, 2, true);
if (myResult !== null) {
let myNonNullResult = myResult;
switch (typeof myNonNullResult) {
case "number":
console.log(`myNonNullResult = ${myNonNullResult}`);
console.log(myNonNullResult.toFixed(2));
break;
case "string":
console.log(`myNonNullResult = ${myNonNullResult}`);
console.log(myNonNullResult.charAt(0));
break;
}
} else {
console.log(`myResult = ${myResult}`);
}
Por último, al haber activado la opción strictNullChecks en el compilador de TypeScript,
no podremos hacer uso de una variable en el código fuente antes de que se encuentre inicializada.
Por ejemplo, trate de compilar el siguiente fragmento de código:
function div(
numerator: number,
denominator: number,
isNumber: boolean,
): number | string | null {
if (denominator === 0) {
return null;
}
return isNumber
? numerator / denominator
: (numerator / denominator).toFixed(2);
}
let myResult: number | string | null;
eval("myResult = div(1, 2, true)");
if (myResult !== null) {
let myNonNullResult = myResult;
switch (typeof myNonNullResult) {
case "number":
console.log(`myNonNullResult = ${myNonNullResult}`);
console.log(myNonNullResult.toFixed(2));
break;
case "string":
console.log(`myNonNullResult = ${myNonNullResult}`);
console.log(myNonNullResult.charAt(0));
break;
}
} else {
console.log(`myResult = ${myResult}`);
}
Al hacerlo, el compilador de TypeScript emite los siguientes errores informando
de que la variable myResult está siendo utilizada antes de haberse inicializado:
src/index.ts:17:5 - error TS2454: Variable 'myResult' is used before being assigned.
17 if (myResult !== null) {
~~~~~~~~
src/index.ts:18:25 - error TS2454: Variable 'myResult' is used before being assigned.
18 let myNonNullResult = myResult;
~~~~~~~~
Found 2 errors in the same file, starting at: src/index.ts:17
Sin embargo, hay veces, como la que nos ocupa, en las que realmente se está asignando un valor
a la variable antes de utilizarla, solo que el compilador no es capaz de visualizarlo.
Tal y como puede observarse en el código anterior, se está utilizando la función eval, la cual
recibe una cadena de caracteres y que permite ejecutar o evaluar el código fuente indicado en
dicha cadena. En este caso, la cadena es "myResult = div(1, 2, true)". Tras la evaluación de
dicha cadena por la función eval, la variable myResult quedará inicializada.
Por lo tanto, para evitar que el compilador emita los errores anteriores, se debe utilizar una
afirmación de asignación definitiva o, en ingles, definitive assignment assertion,
que consiste en utilizar el caracter ! durante la declaración de la variable afectada, justo
después de su nombre, esto es, myResult. De este modo, al declarar la variable myResult, le
estamos indicando al compilador que nos vamos a responsabilizar de que se le asigne un valor
antes de ser utilizada en el código fuente:
function div(
numerator: number,
denominator: number,
isNumber: boolean,
): number | string | null {
if (denominator === 0) {
return null;
}
return isNumber
? numerator / denominator
: (numerator / denominator).toFixed(2);
}
let myResult!: number | string | null;
eval("myResult = div(1, 2, true)");
if (myResult !== null) {
let myNonNullResult = myResult;
switch (typeof myNonNullResult) {
case "number":
console.log(`myNonNullResult = ${myNonNullResult}`);
console.log(myNonNullResult.toFixed(2));
break;
case "string":
console.log(`myNonNullResult = ${myNonNullResult}`);
console.log(myNonNullResult.charAt(0));
break;
}
} else {
console.log(`myResult = ${myResult}`);
}
A pesar de que en esta última parte se está utilizando la función eval para ilustrar
el comportamiento de una afirmación de asignación definitiva, en términos generales, el
uso de esta función está totalmente desaconsejado. Esto se debe a que se pueden producir
errores en tiempo de ejecución como consecuencia de que no se puede llevar a cabo ninguna
comprobación sobre el código fuente que va a ejecutarse y que se ha indicado a través
de la cadena de caracteres argumento de dicha función.