Búsqueda de sitios web

typescript - ¿Cómo puedo proporcionar tipados ergonómicos para un módulo que exporta por defecto un objeto que contiene una clase?


Por "ergonómico" me refiero a "no requiere que los consumidores usen la sintaxis InstanceType<typeof mod["Foo"]>"

Estoy escribiendo un .d.ts para un archivo fuente de JS. En unos meses, ese archivo JS se convertirá a TS pero aún no podemos convertirlo.

El archivo es algo así:

// module.js
class Foo {
    sayFoo(){}
}

export default {
    Foo
};

tsc genera un .d.ts así:

// module.d.ts
declare class Foo {
    sayFoo(): void;
}
declare const _default: {
    Foo: typeof Foo;
};
export default _default;

El llamante tiene que pasar por algunos obstáculos realmente extraños para llegar al tipo de clase:

import mod from "./module"

function takesFoo(foo: InstanceType<typeof mod["Foo"]>) {
    foo.sayFoo();
}

Pero si miento un poco sobre los tipos usando namespace:

export default NS;
// module.d.ts
declare namespace NS {
    export class Foo {
        sayFoo(): void
    }
}

¡entonces el llamador obtiene una API de nivel de tipo mucho mejor!

import mod from "./module";
function takesFoo(foo: mod.Foo) {
    foo.sayFoo();
}

Yo podría simplemente usar namespace, pero entonces estaremos en un aprieto cuando convertimos el archivo JS a TS. Nuestras opciones en ese momento serán:

  • (A) Nunca conviertas realmente el punto de entrada de la biblioteca a TS. Escribe la mayor parte de la biblioteca en TS, luego utiliza un pequeño archivo JS con un .d.ts para envolver la API. ¡Esto eliminaría cualquier verificación de que realmente estamos cumpliendo con nuestra API pública! Y sería confuso.

  • (B) Obliga a los llamadores a usar la extraña API de nivel de tipo. Quizás suaviza el golpe exportando algunas interfaces y enseñando a los llamadores sobre la personalidad dividida de los tipos de clase de TS.

¿Me está faltando una opción o no estoy entendiendo algo sobre los puntos a nivel de tipo en TS?

Respuesta #1

ACTUALIZACIÓN: Puede ser mucho más simple de lo que pensé, aunque mi consejo sobre cómo usar exportaciones por defecto sigue siendo válido... Además, esto puede que en realidad no funcione (actualización adicional: parece estar bien si lo pruebo en un entorno simple generado por create-react-app).

Con tu exportación por defecto existente, esto parece compilar:

import { Foo } from "./module"

const x = new Foo();

function f(y: Foo) {
   y.sayFoo();
}

f(x);

Respuesta original a continuación:

¿Tienes la libertad de añadir export delante de class Foo {?

De esa manera, los usuarios pueden decir:

import { Foo } from "./module";

y luego usa Foo directamente:

function takesFoo(foo: Foo) {

Alternativamente, si Foo es lo único exportado del módulo y deseas que sea la exportación por defecto, tendría más sentido exportarlo solo, en lugar de envolverlo en un objeto:

export default Foo; 

para que los usuarios pudieran importarlo:

import Foo from "./module";

La forma en que pienso sobre la exportación default es que tiene más sentido si solo tengo una cosa para exportar, en cuyo caso ¿por qué envolverla en un objeto? Y si tengo varias cosas para exportar, simplemente las exporto individualmente para que puedan ser importadas con { ... } según sea necesario.