Mecanografiado: comportamiento incorrecto o indulgencias de fiabilidad

El objetivo es mostrar dónde TS da la ilusión de seguridad, lo que le permite obtener errores mientras se ejecuta el programa.

No hablaremos de errores, en TS hay suficientes
1,500 errores abiertos y 6,000 cerrados ('es: el problema es: etiqueta abierta: error').

Todos los ejemplos serán considerados con:

  • El modo estricto de TS está activado (escribió un artículo mientras lo entendía)
  • Sin explícito "any": "as any", "Objects", "Function", {[key: string]: unknown}
  • Sin "any" implícito: (noImplicitAny): importaciones sin tipo (archivos JS puros), inferencia de tipo incorrecta
  • Sin conjeturas falsas sobre los tipos: respuesta del servidor, tipeo de bibliotecas de terceros

Contenido:

  • Introducción
  • Tipos nominales, tipos personalizados: cuando las cosas parecen iguales, pero muy diferentes
  • Variación de tipos, tipos exactos: sobre la relación entre tipos
  • Invalidación de refinamiento - hablar de confianza
  • Excepciones: ¿vale la pena admitirlo cuando está en mal estado?
  • Operaciones inseguras: la confianza no siempre es buena
  • Casos de bonificación - Verificación de tipo en la etapa de revisión de relaciones públicas
  • Conclusión

Introducción


¿Es difícil escribir una función para agregar dos números en JS? Tomar una implementación ingenua

function sum(a, b) {
	return a + b;
}

Veamos nuestra implementación de `sum (2, 2) === 4`, ¿parece que todo funciona? En realidad no, cuando describimos una función, debemos pensar en todo tipo de valores de entrada, así como en lo que la función puede devolver

1.1 + 2.7   // 3.8000000000000003
NaN + 2     // NaN
99999999999999992 + 99999999999999992 // 200000000000000000
2n + 2      // Uncaught TypeError: 
            // Cannot mix BigInt and other types, use explicit conversions.
{} + true   // 1
2 + '2'     // '22'

La solidez es la capacidad del analizador para demostrar que no hay errores mientras se ejecuta el programa. Si el analizador aceptó el programa, se garantiza que es seguro.

El programa seguro es un programa que puede funcionar para siempre sin errores. Aquellos. el programa no se bloqueará ni arrojará errores.

Programa correcto: un programa que hace lo que debería y no hace lo que no debería. La corrección depende de la ejecución de la lógica empresarial.

Los tipos pueden probar que el programa en su conjunto es seguro, y las pruebas de que el programa es seguro y correcto solo dentro de los datos de la prueba (cobertura del 100%, la ausencia de "mutantes" de Stryker, pasar una prueba basada en la propiedad, etc., no pueden probar nada, y las licencias reduce riesgos). Hay leyendas que los demostradores de teoremas pueden probar la exactitud del programa.

Es importante comprender la filosofía TS, comprender qué está tratando de resolver la herramienta y, qué es importante, qué no está tratando de resolver.

Una nota sobre Soundness
TS omite algunas operaciones que no están seguras en la etapa de compilación. Los lugares con comportamiento poco sólido fueron cuidadosamente pensados.

Objetivos de diseño
No es el objetivo de TS: crear un sistema de tipos con una garantía de seguridad, sino centrarse en el equilibrio entre seguridad y productividad

Estructura de ejemplo:
el problema no es un comportamiento seguro, la lista puede no estar completa, esto es lo que encontré en artículos, informes, TS problemas de git.
La propuesta es un tema de TS abierto hace 3-4 años, con un montón de comentarios y explicaciones interesantes por parte de los autores.
Consejo - En mi humilde opinión del autor, lo que el autor considera buenas prácticas

Mecanografía estructural vs nominal


Escritura estructural versus nominal 1. Problema


Escritura estructural : cuando se comparan tipos no se tienen en cuenta sus nombres o el lugar donde se declararon, y los tipos se comparan según la "estructura".

Queremos enviar la carta `sendEmail` a la dirección correcta` ValidatedEmail`, hay una función para verificar la dirección` validateEmail` que devuelve la dirección correcta `ValidatedEmail`. Desafortunadamente, TS le permite enviar cualquier cadena a `sendEmail`, porque `ValidatedEmail` para TS no es diferente de` string`

type ValidatedEmail = string;
declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail("asdf@gmail.com"));

// Should be error!
sendEmail("asdf@gmail.com");

Mecanografía estructural versus nominal 1. Oferta


github.com/microsoft/TypeScript/issues/202
Ingrese la palabra clave `nominal` para que los tipos se verifiquen nominalmente. Ahora podemos prohibir pasar solo `string` donde se espera` ValidatedEmail`

nominal type ValidatedEmail = string;
declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail('asdf@gmail.com'));

// Error!
sendEmail('asdf@gmail.com');

Mecanografía estructural versus nominal 1. Consejo


Podemos crear un tipo `Opaco`, que tomará algo de` T` y le dará unicidad combinándolo con un tipo creado a partir de la `K` pasada. `K` puede ser un símbolo único (` símbolo único`) o una cadena (entonces será necesario asegurarse de que estas cadenas sean únicas).

type Opaque<K extends symbol | string, T> 
	= T & { [X in K]: never };

declare const validatedEmailK: unique symbol;
type ValidatedEmail = Opaque<typeof validatedEmailK, string>;
// type ValidatedEmail = Opaque<'ValidatedEmail', string>;

declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail('asdf@gmail.com'));

// Argument of type '"asdf@gmail.com"' is not assignable
//  to parameter of type 'Opaque<unique symbol, string>'.
sendEmail('asdf@gmail.com');

Escritura estructural versus nominal 2. Problema


Tenemos una clase de dólar y euro, cada una de las clases tiene un método adicional para agregar el dólar al dólar y el euro al euro. Para TS, estas clases son estructuralmente iguales y podemos agregar el dólar al euro.

export class Dollar {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(dollar: Dollar): Dollar {
    return new Dollar(dollar.value + this.value);
  }
}

class Euro {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(euro: Euro): Euro {
    return new Euro(euro.value + this.value);
  }
}

const dollars100 = new Dollar(100);
const euro100 = new Euro(100);

// Correct
dollars100.add(dollars100);
euro100.add(euro100);

// Should be error!
dollars100.add(euro100);

Mecanografía estructural vs nominal 2. Oferta


github.com/microsoft/TypeScript/issues/202
La oración es la misma, con `nominal`, pero desde Dado que las clases pueden convertirse mágicamente en Nominales (más sobre eso más adelante), se consideran las posibilidades de hacer tal transformación de una manera más explícita.

Mecanografía estructural versus nominal 1. Consejo


Si la clase tiene un campo privado (nativo con `#` o de TS c `private`), entonces la clase se convierte mágicamente en Nominal, el nombre y el valor pueden ser cualquier cosa. Utiliza `!` (Asignación de asignación definida) para evitar que TS inscriba en un campo no inicializado (marcas estrictas de verificación, indicadores de inicialización estricta están habilitados).

class Dollar {
  // #desc!: never;
  private desc!: never;
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(dollar: Dollar) {
    return new Dollar(dollar.value + this.value);
  }
}

class Euro {
  // #desc!: never;
  private desc!: never;
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(euro: Euro) {
    return new Euro(euro.value + this.value);
  }
}

const dollars100 = new Dollar(100);
const euro100 = new Euro(100);

// Correct
dollars100.add(dollars100);
euro100.add(euro100);

// Error: Argument of type 'Euro' is not assignable to parameter of type 'Dollar
dollars100.add(euro100);

Tipo de varianza 1. Problema


En resumen, una opción de programación es la capacidad de pasar Supertype / Subtype a donde se espera Type. Por ejemplo, hay una forma de jerarquía -> Círculo -> Rectángulo, ¿es posible transferir o devolver una Forma / Rectángulo si se espera un Círculo?

Variante en programación habr , SO .

Podemos pasar el tipo con el campo en el que se encuentra el número a una función que espera el campo como una cadena o un número, y muta el objeto transmitido en el cuerpo, cambiando el campo a una cadena. Aquellos. `{estado: número} como {estado: número | string} as {status: string} `aquí hay un truco como convertir un número en una cadena, causando un error sorpresa .

function changeStatus(arg: { status: number | string }) {
  arg.status = "NotFound";
}

const error: { status: number } = { status: 404 };
changeStatus(error);

// Error: toFixed is not a function
console.log(error.status.toFixed());

Tipo de variación 1. Oferta


github.com/Microsoft/TypeScript/issues/10717
Se propone introducir `in / out` para limitar explícitamente la covarianza / contravarianza para los genéricos.

function changeStatus<
  out T extends {
    status: number | string;
  }
>(arg: T) {
  arg.status = "NotFound";
}

const error: { status: number } = { status: 404 };
// Error!
changeStatus(error);

console.log(error.status.toFixed());

Tipo de varianza 1. Consejo


Si trabajamos con estructuras inmutables, entonces no habrá tal error (ya hemos habilitado la marca estricta FunctionTypes).

function changeStatus(arg: Readonly<{ status: number | string }>) {
  // Error: Cannot assign, status is not writable
  arg.status = "NotFound";
}

const error: Readonly<{ status: number }> = { status: 404 };
changeStatus(error);

console.log(error.status.toFixed());

Tipo de varianza 1. Bonificación


Readonly es asignable a
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151 mutable .

Pero, incluso si creamos el tipo Readonly, TS no prohibirá pasar a la función donde no esperado Readonly `Readonly <{readonly status: number}> as {status: number | cadena} como {estado: cadena} `

function changeStatus(arg: { status: number | string }) {
  arg.status = "NotFound";
}

const error: Readonly<{ readonly status: number }> 
  = { status: 404 };
changeStatus(error);

// Error: toFixed is not a function
console.log(error.status.toFixed());

Tipo de varianza 2. Problema


Los objetos pueden contener campos adicionales que los tipos que les corresponden no tienen: `{mensaje: cadena; estado: cadena} como {mensaje: cadena} `. Debido a que algunas operaciones pueden no ser seguras

const error: { message: string; status: string } = {
  message: "No data",
  status: "NotFound"
};

function updateError(arg: { message: string }) {
  const defaultError = { message: "Not found", status: 404 };
  const newError: { message: string; status: number }
    = { ...defaultError, ...arg };
  
  // Error: toFixed is not a function
  console.log(newError.status.toFixed());
}

updateError(error);

TS pensó que como resultado de la fusión, `{... {mensaje: cadena, estado: número}, ... {mensaje: cadena}}` el estado será un número.

En realidad, `{... {mensaje:" No encontrado ", estado: 404}, ... {mensaje:" Sin datos ", estado:" NotFound "},}` estado - cadena.

Tipo de variación 2. Oferta


github.com/microsoft/TypeScript/issues/12936
Presentamos un tipo `Exact` o una sintaxis similar para decir que un tipo no puede contener campos adicionales.

const error: Exact<{ message: string; }> = {
  message: "No data",
};

function updateError(arg: Exact<{ message: string }>) {
  const defaultError = {  message: "Not found", status: 404, };
  // Can spread only Exact type!
  const newError = { ...defaultError, ...arg };
  console.log(newError.status.toFixed());
}

updateError(error);

Tipo de variación 2. Consejo


Combine objetos enumerando explícitamente campos o filtrando campos desconocidos.

const error: { message: string; status: string } = {
  message: "No data",
  status: "NotFound"
};

function updateError(arg: { message: string }) {
  const defaultError = { message: "Not found", status: 404 };
  // Merge explicitly or filter unknown fields
  const newError = { ...defaultError, message: arg.message };
  console.log(newError.status.toFixed());
}

updateError(error);

Invalidación de refinamiento. Problema


Después de que hayamos demostrado algo sobre el estado externo, llamar a funciones no es seguro, porque No hay garantías de que las funciones no cambien este estado externo:

export function logAge(name: string, age: number) {
  // 2nd call -  Error: toFixed is not a function
  console.log(`${name} will lose ${age.toFixed()}`);
  person.age = "PLACEHOLDER";
}

const person: { name: string; age: number | string } = {
  name: "Person",
  age: 42
};

if (typeof person.age === "number") {
  logAge(person.name, person.age);
  // refinement should be invalidated
  logAge(person.name, person.age);
}

Invalidación de refinamiento. Frase


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
Agregue el modificador `pure` para funciones, esto al menos le permitirá confiar en tales funciones

Invalidación de refinamiento. Propina


Utilice estructuras de datos inmutables, entonces la llamada a la función será segura a priori para verificaciones previas.

Prima


El tipo de flujo es tan fuerte que no tiene todos los problemas enumerados anteriormente, pero está funcionando tan bien que no recomendaría usarlo.

Excepciones Problema


TS no ayuda a trabajar con Excepciones de ninguna manera; nada está claro en la firma de la función.

import { JokeError } from "../helpers";

function getJoke(isFunny: boolean): string {
  if (isFunny) {
    throw new JokeError("No funny joke");
  }
  return "Duh";
}

const joke: string = getJoke(true);
console.log(joke);

Excepciones Frase


github.com/microsoft/TypeScript/issues/13219
Se propone introducir una sintaxis que permita describir explícitamente Excepciones en una firma de función

import { JokeError } from '../helpers';

function getJoke(isFunny: boolean): string | throws JokeError {
  /*...*/}

function getJokeSafe(isFunny: boolean): string {
  try {
    return getJoke(isFunny);
  } catch (error) {
    if (error instanceof JokeError) {
      return "";
    } else {
      // Should infer error correctly, should cast to never
      return error as never;
    }
  }
}

console.log(getJokeSafe(true));

Excepciones Prima


github.com/microsoft/TypeScript/issues/6283
Por alguna razón, en TS, la definición de tipo para Promise ignora el tipo de error

const promise1: Promise<number> = Promise.resolve(42);

const promise: Promise<never> = Promise.reject(new TypeError());

// typescript/lib
interface PromiseConstructor {
  new <T>(
    executor: (
      resolve: (value?: T | PromiseLike<T>) => void,
      reject: (reason?: any) => void
    ) => void
  ): Promise<T>;
}

Excepciones Propina


Tome un contenedor ya sea como Promise, con solo la mejor escritura. ( Cualquiera de los ejemplos de implementación )

import { Either, exhaustiveCheck, JokeError } from "../helpers";

function getJoke(isFunny: boolean): Either<JokeError, string> {
  if (isFunny) {
    return Either.left(new JokeError("No funny joke"));
  }
  return Either.right("Duh");
}

getJoke(true)
  // (parameter) error: JokeError
  .mapLeft(error => {
    if (error instanceof JokeError) {
      console.log("JokeError");
    } else {
      exhaustiveCheck(error);
    }
  })
  // (parameter) joke: string
  .mapRight(joke => console.log(joke));

Operaciones inseguras. Problema


Si tenemos una tupla de un tamaño fijo, TS puede garantizar que haya algo en el índice solicitado. Esto no funcionará para la matriz y TS confiará en nosotros

// ReadOnly fixed size tuple
export const constNumbers: readonly [1, 2, 3] 
  = [1, 2, 3] as const;

// Error: Object is possibly 'undefined'.
console.log(constNumbers[100].toFixed());

const dynamicNumbers: number[] = [1, 2, 3];
console.log(dynamicNumbers[100].toFixed());

Operaciones inseguras. Frase


github.com/microsoft/TypeScript/issues/13778
Se propone agregar `undefined` al tipo de retorno` T` para acceder a la matriz por índice. Pero en este caso, al acceder a cualquier índice, tendrá que usar `?` O hacer comprobaciones explícitas.

// interface Array<T> {
//   [n: number]: T | undefined;
// }

const dynamicNumbers: number[] = [1, 2, 3];
// Error: Object is possibly 'undefined'.
console.log(dynamicNumbers[100].toFixed());

// Optional chaining `?`
console.log(dynamicNumbers[100]?.toFixed());

// type refinement
if (typeof dynamicNumbers[100] === 'number') {
  console.log(dynamicNumbers[100].toFixed());
}

Operaciones inseguras. Propina


Para no producir entidades más allá de la necesidad, tomamos el contenedor previamente conocido `Either` y escribimos una función segura para trabajar con el índice, que devolverá` Either <null, T>`.

import { Either } from "../helpers";

function safeIndex<T>(
  array: T[],
  index: number,
): Either<null, T> {
  if (index in array) {
    return Either.right(array[index]);
  }
  return Either.left(null);
}

const dynamicNumbers: number[] = [1, 2, 3];

safeIndex(dynamicNumbers, 100)
  .mapLeft(() => console.log("Nothing"))
  .mapRight(el => el + 2)
  .mapRight(el => console.log(el.toFixed()));

Prima Funciones de recarga


Si queremos decir que una función toma un par de líneas y devuelve una cadena o toma un par de números y devuelve un número, entonces en la implementación estas firmas serán contiguas, y el programador debe garantizar su corrección, pero en TS.

PS mira la alternativa a través de tipos genéricos y condicionales:

function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: string | number,
             b: string | number,
): string | number {
  return `${a} + ${b}`;
}

const sum: number = add(2, 2);
// Error: toFixed is not a function
sum.toFixed();


Prima Tipo de guardia


TS confía en el programador que `isSuperUser` determina correctamente quién es` SuperUser` y si se agrega `Vasya`, no habrá avisos.

PS vale la pena pensar en cómo distinguiremos los tipos que ya están en la etapa de su unión - unión etiquetada

type SimpleUser = { name: string };
type SuperUser = { 
  name: string; 
  isAdmin: true; 
  permissions: string[] 
};
type Vasya = { name: string; isAdmin: true; isGod: true };
type User = SimpleUser | SuperUser | Vasya;

function isSuperUser(user: User): user is SuperUser {
  return "isAdmin" in user && user.isAdmin;
}

function doSomethings(user: User) {
  // Error: Cannot read property 'join' of undefined
  if (isSuperUser(user)) {
    console.log(user.permissions.join(","));
  }
}

Conclusiones sobre los consejos


- Tipos nominales : tipo opaco, campos privados
- Variación de tipo : tipos exactos, tipo DeepReadonly
- Excepciones : Cualquiera de mónada
- Invalidación de refinamiento : Funciones puras
- Operaciones inseguras (acceso al índice) : Cualquiera / Quizás mónadas

Datos inmutables, funciones puras, mónadas ... Felicitaciones , demostramos que FP es genial!

recomendaciones


  • TS quiere lograr un equilibrio entre lo correcto y la productividad
  • Las pruebas pueden probar la seguridad y la corrección solo para los datos de la prueba.
  • Los tipos pueden demostrar la seguridad general de un programa.
  • Mutación - mal, ¿de acuerdo?

Como DZ recomendaría jugar con Flow después de haber corregido un error simple:

// https://flow.org/try/
declare function log(arg: { name: string, surname?: string }): void;
const person: { name: string } =  { name: 'Negasi' };
// Error
log(person);

Código de ejemplo, solución y análisis del problema, enlaces útiles en el repositorio .

All Articles