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 suficientes1,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 ingenuafunction 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 devolver1.1 + 2.7
NaN + 2
99999999999999992 + 99999999999999992
2n + 2
{} + true
2 + '2'
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 SoundnessTS 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ñoNo es el objetivo de TS: crear un sistema de tipos con una garantía de seguridad, sino centrarse en el equilibrio entre seguridad y productividadEstructura 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ácticasMecanografí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/202Ingrese 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>;
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);
dollars100.add(dollars100);
euro100.add(euro100);
dollars100.add(euro100);
Mecanografía estructural vs nominal 2. Oferta
github.com/microsoft/TypeScript/issues/202La 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 {
private desc!: never;
value: number;
constructor(value: number) {
this.value = value;
}
add(dollar: Dollar) {
return new Dollar(dollar.value + this.value);
}
}
class Euro {
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);
dollars100.add(dollars100);
euro100.add(euro100);
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);
console.log(error.status.toFixed());
Tipo de variación 1. Oferta
github.com/Microsoft/TypeScript/issues/10717Se 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 };
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 }>) {
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 agithub.com/Microsoft/TypeScript/issues/13347github.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);
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 segurasconst 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 };
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/12936Presentamos 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, };
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 };
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) {
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);
logAge(person.name, person.age);
}
Invalidación de refinamiento. Frase
github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251Agregue el modificador `pure` para funciones, esto al menos le permitirá confiar en tales funcionesInvalidació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/13219Se propone introducir una sintaxis que permita describir explícitamente Excepciones en una firma de funciónimport { 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 {
return error as never;
}
}
}
console.log(getJokeSafe(true));
Excepciones Prima
github.com/microsoft/TypeScript/issues/6283Por alguna razón, en TS, la definición de tipo para Promise ignora el tipo de errorconst promise1: Promise<number> = Promise.resolve(42);
const promise: Promise<never> = Promise.reject(new TypeError());
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)
.mapLeft(error => {
if (error instanceof JokeError) {
console.log("JokeError");
} else {
exhaustiveCheck(error);
}
})
.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
export const constNumbers: readonly [1, 2, 3]
= [1, 2, 3] as const;
console.log(constNumbers[100].toFixed());
const dynamicNumbers: number[] = [1, 2, 3];
console.log(dynamicNumbers[100].toFixed());
Operaciones inseguras. Frase
github.com/microsoft/TypeScript/issues/13778Se 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.
const dynamicNumbers: number[] = [1, 2, 3];
console.log(dynamicNumbers[100].toFixed());
console.log(dynamicNumbers[100]?.toFixed());
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);
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 etiquetadatype 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) {
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ónadasDatos 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:
declare function log(arg: { name: string, surname?: string }): void;
const person: { name: string } = { name: 'Negasi' };
log(person);
Código de ejemplo, solución y análisis del problema, enlaces útiles en el repositorio .