Búsqueda de sitios web

javascript - ¿Por qué TypeScript me permite importar dependencias que no puede usar en tiempo de ejecución?


Puedes ver mi proyecto de ejemplo aquí: https://github.com/DanKaplanSES/typescript-stub-examples/tree/JavaScript-import-invalid

He creado este archivo llamado main.ts:

import uuid from "uuid";

console.log(uuid.v4());

Aunque TypeScript está bien con esta importación, cuando intento node main.js, da este error:

console.log(uuid_1["default"].v4());
                              ^

TypeError: Cannot read property 'v4' of undefined
    at Object.<anonymous> (C:\root\lib\main.js:5:31)
←[90m    at Module._compile (internal/modules/cjs/loader.js:1063:30)←[39m
←[90m    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)←[39m
←[90m    at Module.load (internal/modules/cjs/loader.js:928:32)←[39m
←[90m    at Function.Module._load (internal/modules/cjs/loader.js:769:14)←[39m
←[90m    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)←[39m
←[90m    at internal/main/run_main_module.js:17:47←[39m

Si cambio el archivo a esto, se ejecuta bien:

import * as uuid from "uuid";

console.log(uuid.v4());

Si la primera versión es inválida, ¿por qué TypeScript no me informa?

Tengo una configuración de tsconfig con múltiples archivos. Consulta el proyecto de GitHub para más detalles, pero aquí están las opciones de compilador compartidas que pueden ser relevantes:

{
    "compilerOptions": {
        "rootDir": ".",
        "esModuleInterop": true,
        "module": "CommonJS",
        "moduleResolution": "node",
        "composite": true,
        "importHelpers": true,
    },
}

Así es como se ve el main.js:

no funciona

"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
var uuid_1 = tslib_1.__importDefault(require("uuid"));
console.log(uuid_1["default"].v4());

funciona

"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
var uuid = tslib_1.__importStar(require("uuid"));
console.log(uuid.v4());

Respuesta #1

Me siento un poco culpable por responder a mi propia pregunta de recompensa, así que la voy a marcar como de la comunidad. La razón por la que escribo la mía es porque siento que la otra respuesta realmente oculta lo más importante. Tuve que realizar horas y horas de mi propia investigación después de leerla para poder escribir esto. Dicho esto, creo que mi respuesta será más útil para las personas que empiezan en mi situación, donde yo no sabía lo que no sabía. También creo que hay una solución adicional al problema, aunque sería más invasiva.

Mi pregunta original fue: "Si la primera versión es inválida, ¿por qué TypeScript no me lo informa?" Aquí está la explicación de la otra respuesta:

Porque has activado esModuleInterop, lo que también activa allowSyntheticDefaultImports. El paquete CommonJS en realidad es incompatible con esa opción, pero TypeScript no lo sabe.

Esto es absolutamente cierto, pero cuando se trata de entender lo que está pasando, es solo la punta del iceberg:

Si miras la documentación de referencia, recomienda que configures esModuleInterop en true. ¿Por qué haría esa recomendación si disminuye la seguridad de tipos? Bueno, esa no es la razón por la que recomienda configurarlo en true. De hecho, esta configuración no disminuye la seguridad de tipos: la aumenta al corregir algunos errores antiguos de TypeScript, específicamente dos que tienen que ver con cómo TypeScript maneja requires. Puedes leer la documentación para más detalles sobre eso, pero en mi opinión, si estás usando bibliotecas de Node, creo que es una buena idea configurar esModuleInterop en true.

¡Pero! esModuleInterop tiene un efecto secundario. Al final de su documentación, dice:

Habilitar esModuleInterop también habilitará allowSyntheticDefaultImports.

Eh... más o menos. En mi opinión, esta documentación es incorrecta. Lo que realmente debería decir es: "Habilitar esModuleInterop permitirá por defecto que allowSyntheticDefaultImports sea true." Si miras la documentación de allowSyntheticDefaultImports, dice esto en el lado derecho:

Oye, ¿nota cómo en esa esquina superior derecha no dice que esta configuración es recomendable? Probablemente sea porque esta configuración reduce la seguridad de tipos: permite que escribas import React from "react"; en lugar de import * as React from "react"; cuando el módulo no especifica explícitamente una exportación por defecto.

Normalmente (es decir, con allowSyntheticDefaultImports configurado en falso), esto sería un error... porque lo es: no deberías poder importar por defecto un módulo a menos que tenga una exportación por defecto. Configurar esto en verdadero hace que el compilador diga: "Nah, esto está bien."

Pero, cuando estableces allowSyntheticDefaultImports en true, "esta bandera no afecta el JavaScript emitido por TypeScript." Lo que esto significa es que esta bandera te permite fingir que la biblioteca fue escrita de cierta manera en tiempo de compilación, aunque no lo haya sido. En tiempo de ejecución, esto dará error. ¿Por qué existe esta configuración? No tengo idea, pero probablemente tiene que ver con razones históricas:

Esta opción hace que el comportamiento de TypeScript esté en línea con Babel, donde se emite código adicional para hacer que el uso de una exportación por defecto de un módulo sea más ergonómico.

Parece que hubo (¿hay?) un momento en que se asumía que todos simplemente estaban usando Babel. Yo no estoy haciendo eso, así que el beneficio "ergonómico" se convierte en un error en tiempo de ejecución.

Como un método más limpio, deberías importar uuid con import { v4 } desde 'uuid';

Cierto, pero también creo que sería una buena idea establecer explícitamente allowSyntheticDefaultImports en false. Te da más seguridad de tipo. No solo eso, hace que import uuid from "uuid"; sea un error en tiempo de compilación (como debería ser).

Hay una cosa más que no entiendo:

Configurar allowSyntheticDefaultImports a false también hace que importaciones como import os from "os"; y import _ from "lodash"; sean errores en tiempo de compilación. Pero esas siempre funcionaban bien cuando allowSyntheticDefaultImports era true. Debe haber alguna pieza que me estoy perdiendo que explique por qué esas funcionan pero uuid no.

No puedo encontrar la fuente de os en mis node_modules, pero puedo mirar lodash, y su index.js hace esto:

module.exports = require('./lodash');

En ese archivo requerido, dice esto al final:

...
/*--------------------------------------------------------------------------*/

  // Export lodash.
  var _ = runInContext();

  // Some AMD build optimizers, like r.js, check for condition patterns like:
  if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
    // Expose Lodash on the global object to prevent errors when Lodash is
    // loaded by a script tag in the presence of an AMD loader.
    // See http://requirejs.org/docs/errors.html#mismatch for more details.
    // Use `_.noConflict` to remove Lodash from the global object.
    root._ = _;

    // Define as an anonymous module so, through path mapping, it can be
    // referenced as the "underscore" module.
    define(function() {
      return _;
    });
  }
  // Check for `exports` after `define` in case a build optimizer adds it.
  else if (freeModule) {
    // Export for Node.js.
    (freeModule.exports = _)._ = _;
    // Export for CommonJS support.
    freeExports._ = _;
  }
  else {
    // Export to the global object.
    root._ = _;
  }

Realmente no entiendo qué es lo que hace todo esto, pero creo que esto está definiendo una variable global llamada _ en tiempo de ejecución. Supongo que eso significa, desde la perspectiva de TypeScript, que esto coincide fortuitamente y funciona. Los archivos de declaración de tipos no tienen un valor por defecto, lo que normalmente causaría un error en tiempo de ejecución, pero casi por coincidencia, todo funciona al final porque el JavaScript de Lodash define un _ global. encogerse de hombros Tal vez este sea un patrón que os también use, pero ya he pasado suficientes horas investigando esto, así que dejaré eso para otro día/pregunta.

Respuesta #2

Su problema está relacionado con la interoperabilidad entre los módulos TypeScript/ECMAScript y CommonJS.

Cuando se trata de las diferencias entre los módulos ECMAScript y los módulos CommonJS:

  • Los módulos CommonJS están diseñados para ser importados como const library=require('library'), lo que permite obtener el objeto completo exports de esa biblioteca. No existe la noción de importación por defecto en CommonJS
  • Los módulos de ECMAScript tienen cláusulas export explícitas para cada elemento exportado. También cuentan con una sintaxis de importación por defecto que permite recuperar la exportación default en una variable local.

Para implementar la interoperabilidad entre los módulos CommonJS y la sintaxis de importación predeterminada de TypeScript, los módulos CommonJS pueden tener una propiedad default.

Esa propiedad default incluso puede ser añadida automáticamente por TypeScript cuando esModuleInterop está habilitado (lo que también habilita allowSyntheticDefaultImports). Esta opción añade esta función auxiliar en tiempo de transpilación:

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};

Básicamente, lo que hace esta función es: si el módulo importado tiene la bandera __esModule establecida en true, expórtalo tal cual porque el módulo está destinado a ser usado como un módulo ECMAScript: import { feature } from 'library'. De lo contrario, expórtalo dentro de un objeto contenedor con una propiedad default, lo que permite la sintaxis import localName from 'library'.

El paquete uuid se está construyendo con @babel/plugin-transform-modules-commonjs que incluye la bandera __esModule y te impide usar la sintaxis de importación por defecto. Otros paquetes como lodash no incluyen esta bandera, lo que permite a TypeScript agregar la propiedad default.

Como conclusión, TypeScript ofrece opciones para interoperar con módulos CommonJS heredados, pero estas opciones no funcionan con módulos CommonJS "conscientes de ECMAScript". TypeScript no puede advertir ni generar un error en tiempo de transpilación porque una interfaz de módulo CommonJS no tiene representación más allá del objeto exports, que solo se conoce en tiempo de ejecución.

Artículos relacionados