Seguridad de tipos en JavaScript: Flow y TypeScript

Cualquiera que se ocupe del desarrollo de la interfaz de usuario en una maldita empresa probablemente haya oído hablar de "JavaScript escrito", que significa "TypeScript de Microsoft". Pero además de esta solución, existe al menos un sistema de escritura JS más común, y también de un actor importante en el mundo de las TI. Este es un flujo de Facebook. Debido a mi aversión personal por Microsoft, solía usar siempre el flujo. Objetivamente, esto se explica por una buena integración con los servicios públicos existentes y la facilidad de transición.



Desafortunadamente, debemos admitir que en 2021 el flujo ya es significativamente inferior a TypeScript tanto en popularidad como en soporte de una variedad de utilidades (y bibliotecas), y es hora de enterrarlo en el estante y dejar de masticar cactus.vaya al estándar TypeScript de facto. Pero debajo de esto, me gustaría comparar estas tecnologías, digamos un par (o no un par) de flujo de Facebook.



¿Por qué necesita seguridad de tipos en JavaScript?



JavaScript es un lenguaje maravilloso. No, no así. El ecosistema construido alrededor de JavaScript es genial. Para 2021, realmente admira el hecho de que puede usar las características más modernas del lenguaje y luego, al cambiar una configuración del sistema de compilación, transpilar el archivo ejecutable para admitir su ejecución en versiones anteriores de navegadores, incluido IE8 , no será de noche recuerda. Puede "escribir en HTML" (es decir, JSX), y luego usar la utilidad babel



(o tsc



) reemplazar todas las etiquetas con construcciones JavaScript correctas, como llamar a la biblioteca React (o cualquier otra, pero más sobre eso en otra publicación).



¿Por qué JavaScript es bueno como lenguaje de secuencias de comandos que se ejecuta en su navegador?



  • JavaScript no necesita ser "compilado". Simplemente agrega construcciones de JavaScript y el navegador debe comprenderlas. Esto inmediatamente da un montón de cosas convenientes y casi gratuitas. Por ejemplo, depurar directamente en el navegador, que no es responsabilidad del programador (que no debe olvidar, por ejemplo, incluir un montón de opciones de depuración del compilador y las bibliotecas correspondientes), sino del desarrollador del navegador. No es necesario que espere de 10 a 30 minutos (tiempo real para C / C ++) mientras se compila su proyecto de línea de 10k para intentar escribir algo diferente. Simplemente cambie la línea, vuelva a cargar la página del navegador y observe el nuevo comportamiento del código. Y en el caso de utilizar, por ejemplo, webpack, la página también se volverá a cargar por ti. Muchos navegadores le permiten cambiar el código dentro de la página usando sus herramientas de desarrollo.
  • - . 2021 . Chrome/Firefox, , , 5% (enterprise-) 30% (UI/) , .
  • JavaScript , . — ( worker'). , 100% CPU ( UI ), , , Promise/async/await/etc.
  • Al mismo tiempo, ni siquiera considero la cuestión de por qué JavaScript es importante. Después de todo, con la ayuda de JS, puede: validar formularios, actualizar el contenido de la página sin recargarlo por completo, agregar efectos de comportamiento no estándar, trabajar con audio y video e incluso puede escribir el cliente completo de su aplicación empresarial en JavaScript.


Al igual que con casi cualquier lenguaje de secuencias de comandos (interpretado), en JavaScript puede ... escribir código roto. Si el navegador no llega a este código, entonces no habrá ningún mensaje de error, ninguna advertencia, nada en absoluto. Por un lado, esto es bueno. Si tiene un sitio web grande, incluso un error de sintaxis en el código del controlador de clic de botón no debería provocar que el usuario no cargue el sitio por completo.



Pero, por supuesto, esto es malo. Porque el hecho de que algo no funcione en algún lugar del sitio es malo. Y sería genial, antes de que el código llegue a un sitio de trabajo, verificar todos los scripts en el sitio y asegurarse de que al menos se compilen. E idealmente - y trabajar. Para esto, se utilizan una variedad de conjuntos de utilidades (mi conjunto favorito es npm + webpack + babel / tsc + karma + jsdom + mocha + chai).



Si vivimos en un mundo ideal, entonces todos los scripts de su sitio, incluso los de una línea, están cubiertos con pruebas. Pero, desafortunadamente, el mundo no es ideal, y para toda esa parte del código que no está cubierta por las pruebas, solo podemos confiar en algún tipo de herramientas de verificación automatizadas. Que puede comprobar:



  • JavaScript. , JavaScript, , , . /// .
  • . , , . , :



    var x = null;
    x.foo();
    
          
          





    . — null .


Además de los errores semánticos, puede haber errores aún más terribles: errores lógicos. Cuando el programa se ejecuta sin errores, pero el resultado no es en absoluto el esperado. Clásico con adición de cadenas y números:



console.log( input.value ) // 1
console.log( input.value + 1 ) // 11

      
      





Las herramientas de análisis de código estático existentes (eslint, por ejemplo) pueden intentar rastrear una cantidad significativa de errores potenciales que un programador comete en su código. Por ejemplo:





Tenga en cuenta que todas estas reglas son esencialmente restricciones que el linter impone al programador. Es decir, el linter en realidad reduce las capacidades del lenguaje JavaScript para que el programador cometa menos errores potenciales. Si habilita todas las reglas, será imposible realizar asignaciones en condiciones (aunque JavaScript inicialmente lo permite), usar claves duplicadas en objetos literales e incluso no se puede llamar console.log()



.



La adición de tipos de variables y la verificación de tipos de llamadas son limitaciones adicionales del lenguaje JavaScript para reducir posibles errores.



imagen

Intentando multiplicar un número por una cadena



Un intento de acceder a una propiedad inexistente (no descrita en el tipo) de un objeto

Un intento de acceder a una propiedad inexistente (no descrita en el tipo) de un objeto.



Un intento de acceder a una propiedad inexistente (no descrita en el tipo) de un objeto

Un intento de llamar a una función con un tipo de argumento que no coincide.



Si escribimos este código sin un verificador de tipos, el código se transpila con éxito. Ningún medio de análisis de código estático, si no utiliza (explícita o implícitamente) información sobre los tipos de objetos, no podrá encontrar estos errores.



Es decir, agregar escritura a JavaScript agrega restricciones adicionales al código que escribe el programador, pero le permite encontrar errores que de otro modo ocurrirían durante la ejecución del script (es decir, muy probablemente en el navegador del usuario).



Capacidades de escritura de JavaScript



Flujo Mecanografiado
Capacidad para establecer el tipo de variable, argumento o tipo de retorno de una función
a : number = 5;
function foo( bar : string) : void {
    /*...*/
} 

      
      



Capacidad para describir su tipo de objeto (interfaz)
type MyType {
    foo: string,
    bar: number
}

      
      



Restricción de valores para un tipo
type Suit = "Diamonds" | "Clubs" | "Hearts" | "Spades";

      
      



Extensión de nivel de tipo separada para enumeraciones
enum Direction { Up, Down, Left, Right }

      
      



"Agregar" tipos
type MyType = TypeA & TypeB;

      
      



"Tipos" adicionales para casos complejos
$Keys<T>, $Values<T>, $ReadOnly<T>, $Exact<T>, $Diff<A, B>, $Rest<A, B>, $PropertyType<T, k>, $ElementType<T, K>, $NonMaybeType<T>, $ObjMap<T, F>, $ObjMapi<T, F>, $TupleMap<T, F>, $Call<F, T...>, Class<T>, $Shape<T>, $Exports<T>, $Supertype<T>, $Subtype<T>, Existential Type (*)
      
      



Partial<T>, Required<T>, Readonly<T>, Record<K,T>, Pick<T, K>, Omit<T, K>, Exclude<T, U>, Extract<T, U>, NonNullable<T>, Parameters<T>, ConstructorParameters<T>, ReturnType<T>, InstanceType<T>, ThisParameterType<T>, OmitThisParameter<T>, ThisType<T>

      
      





Ambos motores para el soporte de tipo JavaScript tienen aproximadamente las mismas capacidades. Sin embargo, si viene de lenguajes fuertemente tipados, incluso JavaScript tipado tiene una diferencia muy importante de Java: todos los tipos describen esencialmente interfaces, es decir, una lista de propiedades (y sus tipos y / o argumentos). Y si dos interfaces describen las mismas propiedades (o compatibles), entonces se pueden usar en lugar de la otra. Es decir, el siguiente código es correcto en JavaScript escrito, pero claramente incorrecto en Java o, digamos, C ++:



type MyTypeA = { foo: string; bar: number; }
type MyTypeB = { foo: string; }

function myFunction( arg : MyTypeB ) : string {
    return `Hello, ${arg.foo}!`;
}

const myVar : MyTypeA = { foo: "World", bar: 42 } as MyTypeA;
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Este código es correcto desde el punto de vista de JavaScript escrito, ya que la interfaz MyTypeB requiere una propiedad foo



con un tipo string



, mientras que una variable con la interfaz MyTypeA sí.



Este código se puede reescribir un poco más corto, usando una interfaz literal para una variable myVar



.



type MyTypeB = { foo: string; }

function myFunction( arg : MyTypeB ) : string {
    return `Hello, ${arg.foo}!`;
}

const myVar = { foo: "World", bar: 42 };
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





El tipo de variable myVar



en este ejemplo es una interfaz literal { foo: string, bar: number }



. Sigue siendo compatible con la interfaz esperada de un argumento de arg



función myFunction



, por lo que este código está libre de errores desde el punto de vista de, por ejemplo, TypeScript.



Este comportamiento reduce significativamente el número de problemas al trabajar con diferentes bibliotecas, código personalizado e incluso simplemente llamar a funciones. Un ejemplo típico es cuando alguna biblioteca define opciones válidas y las pasamos como un objeto de opciones:



// -  
interface OptionsType {
    optionA?: string;
    optionB?: number;
}
export function libFunction( arg: number, options = {} as OptionsType) { /*...*/ }

      
      





//   
import {libFunction} from "lib";
libFunction( 42, { optionA: "someValue" } );

      
      





Tenga en cuenta que el tipo OptionsType



no se exporta desde la biblioteca (ni se importa a un código personalizado). Pero esto no le impide llamar a la función utilizando la interfaz literal para el segundo argumento de la options



función y para el sistema de escritura, para verificar la compatibilidad de tipos de este argumento. Intentar hacer algo como esto en Java provocará una clara confusión entre el compilador.



¿Cómo funciona desde la perspectiva del navegador?



Los navegadores no admiten el flujo de Microsoft TypeScript ni de Facebook. Además, las extensiones de lenguaje JavaScript más recientes aún no han encontrado soporte en algunos navegadores. Entonces, ¿cómo se comprueba, en primer lugar, que este código sea correcto y, en segundo lugar, cómo lo ejecuta el navegador?



La respuesta es traspilar. Todo el código JavaScript "no estándar" pasa por un conjunto de utilidades que convierten el código "no estándar" (desconocido para los navegadores) en un conjunto de instrucciones que los navegadores comprenden. Y para escribir, toda la "transformación" consiste en el hecho de que todos los refinamientos de tipo, todas las descripciones de interfaz, todas las restricciones del código simplemente se eliminan. Por ejemplo, el código del ejemplo anterior se convierte en ...



/* : type MyTypeA = { foo: string; bar: number; } */
/* : type MyTypeB = { foo: string; } */

function myFunction( arg /* : : MyTypeB */ ) /* : : string */ {
    return `Hello, ${arg.foo}!`;
}

const myVar /* : : MyTypeA */ = { foo: "World", bar: 42 } /* : as MyTypeA */;
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





esos.

function myFunction( arg ) {
    return `Hello, ${arg.foo}!`;
}
const myVar = { foo: "World", bar: 42 };
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Esta conversión generalmente se realiza de una de las siguientes maneras.





Ejemplos de configuraciones de proyecto para flujo y TypeScript (usando tsc).

Flujo Mecanografiado
webpack.config.js
{
  test: /\.js$/,
  include: /src/,
  exclude: /node_modules/,
  loader: 'babel-loader',
},

      
      



{
  test: /\.(js|ts|tsx)$/,
  exclude: /node_modules/,
  include: /src/,
  loader: 'ts-loader',
},

      
      



Configuración del transpiler
babel.config.js tsconfig.json
module.exports = function( api ) {
  return {
    presets: [
      '@babel/preset-flow',
      '@babel/preset-env',
      '@babel/preset-react',
    ],
  };
};

      
      



{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": false,
    "jsx": "react",
    "lib": ["dom", "es5", "es6"],
    "module": "es2020",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "outDir": "./dist/static",
    "target": "es6"
  },
  "include": ["src/**/*.ts*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

      
      



.flowconfig
[ignore]
<PROJECT_ROOT>/dist/.*
<PROJECT_ROOT>/test/.*
[lints]
untyped-import=off
unclear-type=off
[options]

      
      





La diferencia entre los enfoques babel + strip y tsc es pequeña en términos de ensamblaje. En el primer caso, se usa babel, en el segundo, será tsc.





Pero hay una diferencia si se usa una utilidad como eslint. TypeScript para linchar con eslint tiene su propio conjunto de complementos que le permiten encontrar aún más errores. Pero requieren que al momento del análisis por el linter tenga información sobre los tipos de variables. Para hacer esto, solo se debe usar tsc como analizador de código, no babel. Pero si se usa tsc para el linter, entonces será incorrecto usar babel para construir (¡el zoológico de utilidades utilizadas debería ser mínimo!).





Flujo Mecanografiado
.eslint.js
module.exports = {
  parser: 'babel-eslint',
  parserOptions: {
    /* ... */

      
      



module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    /* ... */

      
      





Tipos para bibliotecas



Cuando se publica una biblioteca en el repositorio npm, es la versión de JavaScript la que se publica. Se supone que no es necesario modificar el código publicado para poder utilizarlo en un proyecto. Es decir, el código ya ha pasado la traspilación necesaria a través de babel o tsc. Pero entonces la información sobre los tipos en el código ya se pierde. ¿Qué hacer?



En el flujo, se supone que además de la versión "pura" de JavaScript, la biblioteca contendrá archivos con la extensión .js.flow



que contiene el código de flujo fuente con todas las definiciones de tipo. Luego, al analizar el flujo, podrá conectar estos archivos para la verificación de tipos, y al construir el proyecto y su ejecución, serán ignorados: se usarán archivos JS normales. Puede agregar archivos .flow a la biblioteca simplemente copiando. Sin embargo, esto aumentará significativamente el tamaño de la biblioteca en npm.



En TypeScript, no se sugiere mantener los archivos fuente uno al lado del otro, sino solo una lista de definiciones. Si hay un archivo myModule.js



, al analizar el proyecto, TypeScript buscará un archivo cercano myModule.js.d.ts



, en el que espera ver definiciones (¡pero no código!) De todos los tipos, funciones y otras cosas que se necesitan para analizar tipos. El transpilador tsc puede crear dichos archivos a partir del TypeScript de origen por sí solo (consulte la opción declaration



en la documentación).



Tipos de bibliotecas heredadas



Tanto para el flujo como para TypeScript, existe una forma de agregar declaraciones de tipo para aquellas bibliotecas que inicialmente no contienen estas descripciones. Pero se hace de diferentes formas.



Para el flujo, no existe un método "nativo" compatible con el propio Facebook. Pero hay un proyecto de tipo de flujo que recopila tales definiciones en su repositorio. De hecho, una forma paralela de npm para versionar tales definiciones, y tampoco una forma "centralizada" muy conveniente de actualización.



En TypeScript, la forma estándar de escribir tales definiciones es publicarlas en paquetes especiales npm con el prefijo "@types".... Para agregar una descripción de los tipos de una biblioteca a su proyecto, basta con conectar la biblioteca @ types correspondiente, por ejemplo, @types/react



para React o @types/chai



para chai.



Comparación de flujo y TypeScript



Un intento de comparar el flujo y TypeScript. Los datos seleccionados se recopilan del artículo de Nathan Sebhastian "TypeScript VS Flow", algunos se recopilan de forma independiente.



Soporte nativo en varios marcos. Nativo: sin un enfoque adicional con un soldador y bibliotecas y complementos de terceros.



Varios gobernantes

Flujo Mecanografiado
Contribuyente principal Facebook Microsoft
Sitio web flow.org www.typescriptlang.org
Github github.com/facebook/flow github.com/microsoft/TypeScript
Inicio de GitHub 21,3 km 70,1 mil
Bifurcaciones de GitHub 1.8k 9,2 km
Problemas de GitHub: abierto / cerrado 2,4 k / 4,1 k 4.9k / 25.0k
StackOverflow activo 2289 146,221
StackOverflow Frequent 123 11451


Al observar estas cifras, simplemente no tengo el derecho moral de recomendar el uso de flow. Pero, ¿por qué lo usé yo mismo? Porque solía existir el tiempo de ejecución de flujo.



tiempo de ejecución de flujo



flow-runtime es un conjunto de complementos para babel que le permite incrustar tipos de flujo en tiempo de ejecución, usarlos para definir tipos de variables en tiempo de ejecución y, lo más importante para mí, permitirle verificar los tipos de variables en tiempo de ejecución. Eso permitió en tiempo de ejecución durante, por ejemplo, pruebas automáticas o pruebas manuales, para detectar errores adicionales en la aplicación.



Es decir, justo en el tiempo de ejecución (en el ensamblaje de depuración, por supuesto), la aplicación verificó explícitamente todos los tipos de variables, argumentos, resultados de llamadas a funciones de terceros y todo, todo, todo, para cumplir con esos tipos.



Desafortunadamente, para el nuevo año 2021, el autor del repositorio agregó información.que ya no está involucrado en el desarrollo de este proyecto y, en general, cambia a TypeScript. De hecho, la última razón para mantener el flujo se volvió obsoleta para mí. Bien, bienvenido a TypeScript.



All Articles