Post Top Ad

Your Ad Spot

domingo, 28 de junio de 2020

Enumeraciones de TypeScript: ¿cómo funcionan? ¿Para qué se pueden usar?

Esta publicación de blog responde las siguientes dos preguntas:
  • ¿Cómo funcionan las enumeraciones de TypeScript?
  • ¿Para qué se pueden usar?

Tabla de contenido:

Los fundamentos   

JavaScript tiene un tipo con una cantidad finita de valores: boolean, que tiene los valores de truefalsey no hay otros valores. Con enumeraciones, TypeScript le permite definir tipos similares estáticamente usted mismo.

Enumeraciones numéricas   

Este es un ejemplo simple de una enumeración:
enum NoYes {
  No,
  Yes, // trailing comma
}
Las entradas NoYesse llaman los miembros de la enumeración NoYesComo en los literales de objeto, las comas finales se permiten e ignoran.
Podemos utilizar los miembros como si fueran literales tales como true123'abc'- por ejemplo:
function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}
assert.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja');

Valores de miembros de enumeración   

Cada miembro de la enumeración tiene un nombre y un valor . El valor predeterminado para las enumeraciones es ser numérico . Es decir, cada valor de miembro es un número:
enum NoYes {
  No,
  Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);
En lugar de que TypeScript especifique valores de miembro de enumeración para nosotros, también podemos especificarlos nosotros mismos:
enum NoYes {
  No = 0,
  Yes = 1,
}
Este tipo de especificación explícita a través de un signo igual se denomina inicializador .
Podemos omitir el valor de un miembro si el valor del miembro anterior es un número. Luego, TypeScript incrementa ese valor en uno y lo usa para el miembro actual:
enum Enum {
  A,
  B,
  C = 4,
  D,
  E = 8,
  F,
}
assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 4, 5, 8, 9]
);

Carcasa de nombres de miembros enum   

Existen varios precedentes para nombrar constantes (en enumeraciones o en otros lugares):
  • Tradicionalmente, JavaScript ha usado nombres en mayúsculas, que es una convención que heredó de Java y C: Number.MAX_VALUE
  • Los símbolos conocidos son en camello y comienzan con letras minúsculas porque están relacionados con nombres de propiedades: Symbol.asyncIterator
  • El manual de TypeScript utiliza nombres en camello que comienzan con letras mayúsculas. Este es el estilo estándar de TypeScript y lo usamos para la NoYesenumeración.

Citando nombres de miembros enum   

Similar a los objetos de JavaScript, podemos citar los nombres de los miembros de enum:
enum HttpRequestField {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);
No hay forma de calcular los nombres de los miembros de enumeración. Los literales de objeto admiten nombres calculados mediante corchetes.

Enums basados ​​en cadenas   

En lugar de números, también podemos usar cadenas como valores de miembro enum:
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');
Si una enumeración está completamente basada en cadenas, no podemos omitir ningún inicializador.

Enumeraciones heterogéneas   

El último tipo de enumeraciones se llama heterogéneo . Los valores de miembro de una enumeración heterogénea son una mezcla de números y cadenas:
enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8,
  F,
}
assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 'C', 'D', 8, 9]
);
Tenga en cuenta que la regla mencionada anteriormente también se aplica aquí: solo podemos omitir un inicializador si el valor del miembro anterior es un número.
Las enumeraciones heterogéneas no se usan con frecuencia porque tienen pocas aplicaciones.
Por desgracia, TypeScript solo admite números y cadenas como valores de miembro de enumeración. Otros valores, como los símbolos, no están permitidos.

Especificar valores de miembro de enumeración   

TypeScript distingue tres formas de especificar valores de miembros enum:
  • Los miembros de enumeración literal se inicializan:
    • implícitamente o
    • a través de literales numéricos o literales de cadena (explícitamente). Hasta ahora, solo hemos usado miembros literales.
  • Los miembros de enumeración constante se inicializan mediante expresiones cuyos resultados se pueden calcular en tiempo de compilación.
  • Los miembros de enumeración calculados se inicializan mediante expresiones arbitrarias.
En esta lista, las entradas anteriores son menos flexibles, pero admiten más funciones. Las siguientes subsecciones cubren cada entrada con más detalle.

Miembros de enumeración literal   

Un miembro de enumeración es literal si se especifica su valor:
  • implícitamente o
  • a través de un número literal (incluidos los literales de números negados) o
  • a través de una cadena literal.
Si una enumeración solo tiene miembros literales, podemos usar esos miembros como tipos (similar a cómo, por ejemplo, los literales numéricos se pueden usar como tipos):
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(x: NoYes.No) {
  return x;
}

func(NoYes.No); // OK

//@ts-ignore: Argument of type '"No"' is not assignable to
//            parameter of type 'NoYes.No'.
func('No');

//@ts-ignore: Argument of type 'NoYes.Yes' is not assignable to
//            parameter of type 'NoYes.No'.
func(NoYes.Yes);
Además, las enumeraciones literales admiten comprobaciones de exhaustividad (que veremos más adelante).

Miembros de enumeración constante   

Un miembro enum es constante si su valor se puede calcular en tiempo de compilación. Por lo tanto, podemos especificar su valor implícitamente (es decir, dejamos que TypeScript lo especifique por nosotros). O podemos especificarlo explícitamente y solo podemos usar la siguiente sintaxis:
  • Literales de número o literales de cadena
  • Una referencia a un miembro de enumeración constante previamente definido (en la enumeración actual o en una enumeración anterior)
  • Paréntesis
  • Los operadores unitarios +-,~
  • Los operadores binarios +-*/%<<>>>>>&|,^
Este es un ejemplo de una enumeración cuyos miembros son todos constantes (veremos pronto cómo se utiliza esa enumeración):
enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}
Si una enumeración solo tiene miembros constantes, ya no podemos usar miembros como tipos. Pero aún podemos hacer verificaciones de exhaustividad.

Miembros de enumeración calculados   

Los valores de los miembros de enumeración calculados se pueden especificar mediante expresiones arbitrarias. Por ejemplo:
enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}
Esta fue una enumeración numérica. Las enumeraciones basadas en cadenas y las enumeraciones heterogéneas son más limitadas. Por ejemplo, no podemos usar invocaciones de métodos para especificar valores de miembros:
enum NoYesStr {
  No = 'No',
  //@ts-ignore: Computed values are not permitted in
  //            an enum with string valued members.
  Yes = ['Y', 'e', 's'].join(''),
}

Desventajas de las enumeraciones numéricas   

Desventaja: registro   

Al registrar miembros de enumeraciones numéricas, solo vemos números:
enum NoYes { No, Yes }

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 0
// 1

Desventaja: comprobación de tipo suelto   

Cuando se usa la enumeración como tipo, los valores que están permitidos estáticamente no son solo los de los miembros de la enumeración: se acepta cualquier número:
enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); // no error!
¿Por qué no hay controles estáticos más estrictos? Daniel Rosenwasser explica :
El comportamiento está motivado por operaciones bit a bit. Hay momentos en que SomeFlag.Foo | SomeFlag.Barse pretende producir otro SomeFlagEn cambio, terminas con number, y no quieres tener que volver a lanzar SomeFlag.
Creo que si hiciéramos TypeScript de nuevo y aún tuviéramos enumeraciones, habríamos hecho una construcción separada para las banderas de bits.
La forma en que se utilizan las enumeraciones para los patrones de bits se demuestra pronto con más detalle.

Recomendación: prefiera enumeraciones basadas en cadenas   

Mi recomendación es preferir enumeraciones basadas en cadenas (por razones de brevedad, esta publicación de blog no siempre sigue esta recomendación):
enum NoYes { No='No', Yes='Yes' }
Por un lado, la salida de registro es más útil para los humanos:
console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 'No'
// 'Yes'
Por otro lado, tenemos una verificación de tipo más estricta:
function func(noYes: NoYes) {}

//@ts-ignore: Argument of type '"abc"' is not assignable
//            to parameter of type 'NoYes'.
func('abc');
//@ts-ignore: Argument of type '"Yes"' is not assignable
//            to parameter of type 'NoYes'.
func('Yes');

Casos de uso para enumeraciones   

Caso de uso: patrones de bits   

En el módulo del sistema de archivos Node.js , varias funciones tienen el modo de parámetro. Su valor se utiliza para especificar permisos de archivo, a través de una codificación que es un remanente de Unix:
  • Los permisos se especifican para tres categorías de usuarios:
    • Usuario: el propietario del archivo
    • Grupo: los miembros del grupo asociado con el archivo
    • Todos: todos
  • Por categoría, se pueden otorgar los siguientes permisos:
    • r (leer): los usuarios de la categoría pueden leer el archivo
    • w (escribir): los usuarios de la categoría pueden cambiar el archivo
    • x (ejecutar): los usuarios de la categoría pueden ejecutar el archivo
Eso significa que los permisos se pueden representar por 9 bits (3 categorías con 3 permisos cada una):
UsuarioGrupoTodas
Permisosr, w, xr, w, xr, w, x
Poco8, 7, 65, 4, 32, 1, 0
Node.js no hace esto, pero podríamos usar una enumeración para trabajar con estas banderas:
enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}
Los patrones de bits se combinan mediante bit a bit O :
// User can change, read and execute; everyone else can only read and execute
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
  Perm.GroupRead | Perm.GroupExecute |
  Perm.AllRead | Perm.AllExecute,
  0o755);

// User can read and write; group members can read; everyone can’t access at all.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
  0o640);

Una alternativa a los patrones de bits   

La idea principal detrás de los patrones de bits es que hay un conjunto de banderas y que se puede elegir cualquier subconjunto de esas banderas.
Por lo tanto, usar conjuntos reales para elegir subconjuntos es una forma más autodescriptiva de realizar la misma tarea:
enum Perm {
  UserRead,
  UserWrite,
  UserExecute,
  GroupRead,
  GroupWrite,
  GroupExecute,
  AllRead,
  AllWrite,
  AllExecute,
}
function writeFileSync(
  thePath: string, permissions: Set<Perm>, content: string) {
  // ···
}
writeFileSync(
  '/tmp/hello.txt',
  new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
  'Hello!');

Caso de uso: constantes múltiples   

A veces, tenemos conjuntos de constantes que van juntas:
// Log level:
const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');
Este es un buen caso de uso para una enumeración:
enum LogLevel {
  off = 'off',
  info = 'info',
  warn = 'warn',
  error = 'error',
}
Los beneficios de esta enumeración son:
  • Los nombres constantes se agrupan y anidan dentro del espacio de nombres LogLevel.
  • Podemos usar el tipo LogLevelsiempre que necesitemos una de esas constantes y TypeScript comprueba estáticamente que no se utilizan otros valores.

Caso de uso: más autodescriptivo que booleanos   

Cuando los booleanos se utilizan para representar alternativas, las enumeraciones suelen ser una opción más descriptiva.

Ejemplo booleano: listas ordenadas vs. no ordenadas   

Por ejemplo, para representar si una lista está ordenada o no, podemos usar un booleano:
class List1 {
  isOrdered: boolean;
  // ···
}
Sin embargo, una enumeración es más descriptiva y tiene el beneficio adicional de que podemos agregar más alternativas más adelante si es necesario.
enum ListKind { ordered, unordered }
class List2 {
  listKind: ListKind;
  // ···
}

Ejemplo booleano: fracaso vs. éxito   

Del mismo modo, podemos codificar si una operación tuvo éxito o falló a través de un valor booleano o una enumeración:
class Result1 {
  success: boolean;
  // ···
}

enum ResultStatus { failure, success }
class Result2 {
  status: ResultStatus;
  // ···
}

Caso de uso: constantes de cadena más seguras   

Considere la siguiente función que crea expresiones regulares.
const GLOBAL = 'g';
const NOT_GLOBAL = '';
type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;

function createRegExp(source: string,
  globalness: Globalness = NOT_GLOBAL) {
    return new RegExp(source, 'u' + globalness);
  }

assert.deepEqual(
  createRegExp('abc', GLOBAL),
  /abc/ug);
Usar una enumeración basada en cadenas es más conveniente:
enum Globalness {
  Global = 'g',
  notGlobal = '',
}

function createRegExp(source: string, globalness = Globalness.notGlobal) {
  return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', Globalness.Global),
  /abc/ug);

Enums en tiempo de ejecución   

TypeScript compila enumeraciones a objetos JavaScript. Como ejemplo, tome la siguiente enumeración:
enum NoYes {
  No,
  Yes,
}
TypeScript compila esta enumeración para:
var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));
En este código, se realizan las siguientes asignaciones:
NoYes["No"] = 0;
NoYes["Yes"] = 1;

NoYes[0] = "No";
NoYes[1] = "Yes";
Hay dos grupos de tareas:
  • Las dos primeras asignaciones asignan nombres de miembros de enumeración a valores.
  • Las dos segundas asignaciones asignan valores a nombres. Eso permite mapeos inversos , que veremos a continuación.

Mapeos inversos   

Dado un enum numérico:
enum NoYes {
  No,
  Yes,
}
La asignación normal es de nombres de miembros a valores de miembros:
// Static (= fixed) lookup:
assert.equal(NoYes.Yes, 1);

// Dynamic lookup:
assert.equal(NoYes['Yes'], 1);
Las enumeraciones numéricas también admiten una asignación inversa de valores de miembros a nombres de miembros:
assert.equal(NoYes[1], 'Yes');

Enumeraciones basadas en cadenas en tiempo de ejecución   

Las enumeraciones basadas en cadenas tienen una representación más simple en tiempo de ejecución.
Considere la siguiente enumeración.
enum NoYes {
  No = 'NO!',
  Yes = 'YES!',
}
Está compilado con este código JavaScript:
var NoYes;
(function (NoYes) {
    NoYes["No"] = "NO!";
    NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));
TypeScript no admite asignaciones inversas para enumeraciones basadas en cadenas.

constenumeraciones   

Si una enumeración tiene el prefijo de la palabra clave const, no tiene una representación en tiempo de ejecución. En cambio, los valores de su miembro se usan directamente.

Compilación de enumeraciones no constantes   

Para observar este efecto, examinemos primero la siguiente enumeración no constante:
enum NoYes {
  No,
  Yes,
}
function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}
TypeScript compila este código para:
var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));

function toGerman(value) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

Compilando enumeraciones constantes   

Este es el mismo código que antes, pero ahora la enumeración es constante:
const enum NoYes {
  No,
  Yes,
}
function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}
Ahora la representación de la enumeración como una construcción desaparece y solo quedan los valores de sus miembros:
function toGerman(value) {
  switch (value) {
    case 0 /* No */:
      return 'Nein';
    case 1 /* Yes */:
      return 'Ja';
  }
}

Enums en tiempo de compilación   

Las enumeraciones son objetos   

TypeScript trata las enumeraciones (no constantes) como si fueran objetos:
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(obj: { No: string }) {
  return obj.No;
}
assert.equal(
  func(NoYes), // allowed statically!
  'No');

Verificaciones de exhaustividad para enumeraciones literales   

Cuando aceptamos un valor de miembro enum, a menudo queremos asegurarnos de que:
  • No recibimos valores ilegales.
  • No olvidamos considerar ningún valor de miembro enum. (Esto se vuelve especialmente relevante si agregamos nuevos valores de miembro enum más adelante).

Protección contra valores ilegales   

En el siguiente código, tomamos dos medidas contra los valores ilegales:
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new TypeError('Unsupported value: ' + JSON.stringify(value));
  }
}

assert.throws(
  //@ts-ignore: Argument of type '"Maybe"' is not assignable to
  //            parameter of type 'NoYes'.
  () => toGerman('Maybe'),
  /^TypeError: Unsupported value: "Maybe"$/);
Las medidas son:
  • En tiempo de compilación, el tipo NoYesevita que se pasen valores ilegales al parámetro value.
  • En tiempo de ejecución, el defaultcaso se usa para generar una excepción si hay un valor inesperado.

Protección contra el olvido de casos mediante comprobaciones de exhaustividad   

Podemos tomar una medida más. El siguiente código realiza una verificación de exhaustividad : TypeScript nos avisará si olvidamos considerar todos los miembros de enumeración.
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function throwUnsupportedValue(value: never): never {
  throw new TypeError('Unsupported value: ' + value);
}

function toGerman2(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throwUnsupportedValue(value);
  }
}
¿Cómo funciona el control de exhaustividad? Para cada caso, TypeScript infiere el tipo de value:
function toGerman2b(value: NoYes) {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'Nein';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'Ja';
    default:
      const z: never = value;
      throwUnsupportedValue(value);
  }
}
En el caso por defecto, mecanografiado infiere el tipo neverde valueporque nunca lleguemos. Sin embargo, si agregamos un miembro .MaybeNoYes, entonces el tipo inferido de valuees NoYes.MaybeY ese tipo es estáticamente incompatible con el tipo neverdel parámetro de throwUnsupportedValue()Por lo tanto, recibimos el siguiente mensaje de error en tiempo de compilación:
El argumento del tipo 'NoSí.Maybe' no se puede asignar al parámetro del tipo 'nunca'.
Convenientemente, este tipo de verificación de exhaustividad también funciona con ifdeclaraciones:
function toGerman3(value: NoYes) {
  if (value === NoYes.No) {
    return 'Nein';
  } else if (value === NoYes.Yes) {
    return 'Ja';
  } else {
    throwUnsupportedValue(value);
  }
}

Una forma alternativa de verificar la exhaustividad   

Alternativamente, también obtenemos una verificación de exhaustividad si especificamos un tipo de retorno para toGerman():
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function toGerman(value: NoYes): string {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'Nein';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'Ja';
  }
}
Si agregamos un miembro NoYes, TypeScript se queja de que toGerman()puede volver undefined.
Desventaja de este enfoque: Por desgracia, este enfoque no funciona con ifdeclaraciones ( más información ).

keyofy enumeraciones   

Podemos usar el keyofoperador de tipo para crear el tipo cuyos elementos son las claves de los miembros de la enumeración. Cuando lo hacemos, debemos combinarlo keyofcon typeof:
enum HttpRequestKeyEnum {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;
  // = 'Accept' | 'Accept-Charset' | 'Accept-Datetime' |
  //   'Accept-Encoding' | 'Accept-Language'

function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
  // ···
}
¿Por qué hacer esto? Puede ser más conveniente que definir el tipo HttpRequestKeydirectamente.

Utilizando keyofsin typeof  

Si usamos keyofsin typeof, obtenemos un tipo diferente, menos útil:
type Keys = keyof HttpRequestKeyEnum;
  // = 'toString' | 'toFixed' | 'toExponential' |
  //   'toPrecision' | 'valueOf' | 'toLocaleString'
keyof HttpRequestKeyEnumes el mismo que keyof number.

Reconocimiento   

  • Gracias al usuario de Disqus @spira_mirabilispor sus comentarios a esta publicación de blog.

No hay comentarios.:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

outbrain

Páginas