Sharing TypeScript type declarations via npm package
Asked Answered
S

2

11

I'm in need of sharing some TypeScript types between my React client and my Express REST API in order to keep the code clean and DRY. Since this is a private proejct I wouldn't share these types through the @types repository, so I've followed the guide on the TypeScript website and this is the result...

Everything is working just fine in the React client: I've installed the types as a dev dependency and used them flawlessly.

In the Express API I get this error and I presume it has something to do with how I structured my package.

What am I doing wrong? As ignorant as I am I'd suppose it's related with how the modules are loaded, but I can't figure out precisely what may be causing the error.

> cross-env NODE_ENV=production node dist/index.js

internal/modules/cjs/loader.js:834
  throw err;
  ^

Error: Cannot find module '@revodigital/suiteods-types'

How I import the module inside the API code

import { AuthEntity, Roles } from '@revodigital/suiteods-types';

@Model()
export class AuthEntityModel implements AuthEntity {
  /* ... */

  role: Roles;

  /* ... */
}


Package tree
suiteods-types
  |_index.d.ts
  |_package.json
  |_README.md
  |_tsconfig.json

index.d.ts

export = Ods;
export as namespace Ods;

declare namespace Ods {
  /* ... */
  interface AuthEntity extends DomainObject {
    email: string;
    password: string;
    role: Roles;
    instanceId: string;
  }

  enum Roles {
    BASE,
    STUDENT,
    BUSINESS,
    INSTRUCTOR,
    ADMIN
  }
  /* ... */
}

package.json

{
  "name": "@revodigital/suiteods-types",
  "version": "0.1.1",
  "description": "Type declarations for suiteods project",
  "types": "index.d.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Revo Digital",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/revodigital/suiteods-types.git"
  },
  "bugs": {
    "url": "https://github.com/revodigital/suiteods-types/issues"
  },
  "homepage": "https://github.com/revodigital/suiteods-types#readme"
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "lib": [
      "es6"
    ],
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "baseUrl": "../",
    "typeRoots": [
      "../"
    ],
    "types": [],
    "noEmit": true,
    "forceConsistentCasingInFileNames": true
  },
  "files": [
    "index.d.ts"
  ]
}

Update

Confused on how to get rid of the namespace, and still getting the same error on the module, now installed as `dependency` and not as `devDependency`. The file structure is the same as above. Thanks in advance for the help.

updated and complete index.d.ts

export = Ods;
export as namespace Ods;

declare namespace Ods {

  type IdType = 'Carta Identità' | 'Passaporto' | 'Patente'
  
  type ODSModule = 'SCUOLA' | 'OPERATORI' | 'DRONI' | 'ODS_ROOT'

  type Role = 'BASE' | 'STUDENTE' | 'ISTRUTTORE' | 'AMMINISTRATORE' | 'UTENTE_AZIENDALE'
  
  type UserScope = 'INTERNAL' | 'WHOLE'


  interface Address {
    street: string;
    city: string;
    province: string;
    CAP: string;
  }

  interface Credentials {
    email: string;
    password: string;
  }

  interface LoggedEntity {
    authEntity: AuthEntity;
    baseUser: BaseUser;
  }

  interface ModulesInstancesMap {
    SCUOLA: string;
    OPERATORI: string;
    DRONI: string;
    ODS_ROOT: string;
  }

  interface MultiTenantController {

  }

  interface Tenant {
    _id: string;
    role: Role | ODSModule;
  }

  interface TenantInfo {
    tenant: Tenant;
    relativeGodRole: Role;
  }

  interface AuthEntity extends DomainObject {
    email: string;
    password: string;
    role: Role;
    instanceId: string;
  }

  interface BaseUser extends DomainObject {
    firstName: string;
    lastName: string;
    phone: string;
    address: Address;
    scope: UserScope;
  }

  interface BelongsToModule {
    module: ODSModule;
  }

  interface Business extends DomainObject {
    businessName: string;
    pIva: string;
    tel: string;
    pec: string;
    recipientCode: string;
    address: Address;
  }

  interface DomainObject {
    _id: string;
  }

  interface HasTenant {
    tenantInfo: TenantInfo;
  }

  interface Instructor extends BaseUser {
    licenseCode: string;
  }

  interface InternalWholeSuiteUser extends BaseUser {
    modulesInstancesMap: ModulesInstancesMap;
  }

  interface InternalModuleUser extends BaseUser, BelongsToModule {
    moduleInstanceId: string;
  }

  interface School extends Business, HasTenant {
    cApr: number;
  }


  interface Student extends BaseUser {
    stateIssuedIdNumber: string;
    stateIssuedIsType: IdType;
    job: string;
    businessId?: string;
  }
}

Shorts answered 8/3, 2021 at 11:33 Comment(7)
AuthEntity and Roles are interfaces?Bergquist
AuthEntity is an interface, Roles is an enumShorts
@Bergquist got any suggestion? I really am disoriented about this topic and I'd like to better understand what I'm doing wrong.Shorts
In files packages.json of the frontend and the backend, can we see the lines related to @revodigital/suiteods-types? Are they in devDependencies or in dependencies?Bergquist
They're located in devDependencies in both the client and the serverShorts
Mind sharing a code sample anyone?Letishaletitia
I've added a link to the GH repo in my answer @MarcellodeSalesShorts
S
11

In addition to the precious @Paleo edits, adding an index.js file with the content that follows solved the issue.


index.js

module.exports = {};

Updated file structure

suiteods-types
  |_index.d.ts
  |_index.js
  |_package.json
  |_README.md
  |_tsconfig.json

See GitHub repo for full code.


So... How to share TS Types via npm

If you want to share some TypeScript types (E.G. between a client and a server like in my case) the steps (that worked for me) are the ones that follows.
  1. Create a new folder and init it as an npm package with npm init
  2. Extrapolate all the types you want to share between the entities and group them in an index.d.ts file
  3. Make the declaration "pure types"-only, in my case converting the enums into types (and doing a bit of refactoring to adapt the rest of the code) was enough
  4. Add tsconfig.json (see my question above for an example)
  5. Add an index.js containing only module.exports = {}
  6. Publish it (see links below)
  7. Install it as dependency, so `npm i --save @yourscope/yourpkg
  8. Consume it when needed

To publish the package I've used npm and GitHub packages. See these links...
Shorts answered 8/3, 2021 at 22:42 Comment(3)
Needed this to get my package.json "main" entry working. Previously didn't even have an index.js file cause I thought I didn't need it. This helped! Thanks!Ouzo
Can you elaborate on Step 2? What would this index.d.ts look like? Do I just import all the types in to it? Do do I need to export anything?Deafanddumb
@BradleyFlood your index.d.ts should just contain all your types exported one by one with export MyInterface {...}, since as @Bergquist said the namespace used in my question is not ideal. At the GitHub repo linked in the answer you'll find my index file that could be useful.Shorts
B
6

The problem

An enum type is not a pure type. The TypeScript compiler generates some JavaScript code for this type. The rest of your code needs it.

At run time, after a normal deployment, your code can't access to the "dev dependencies". Only the dependencies have been installed.

In the case of your frontend, there is a little magic due to Webpack. At build time, Webpack follows the code in all the dependencies (including dev dependencies), and packs them. So the compiled code of your private dependency is in the bundle and it works.

Solutions

Solution 1 : It is possible to publish your package @revodigital/suiteods-types with just the javascript code used at runtime. And then the package can be used as a regular dependency.

Solution 2 : It is possible to use a bundler (Webpack or Rollup) in the back-end to pack the used code. The private package will be packed the same way as in the front-end.

Solution 3 : Make the types in the private package "pure types" so it won't be needed at all at runtime. Replace all the enum types by unions of strings.

For example:

enum Roles {
    BASE,
    STUDENT,
    BUSINESS,
    INSTRUCTOR,
    ADMIN
  }

… could be replaced by:

type Role = "BASE" | "STUDENT" | "BUSINESS" | "INSTRUCTOR" | "ADMIN"

Notice: it will require some refactoring.

A free advice as a bonus: Do not keep the namespace

It is not recommended to use a namespace in modules. You should get rid of it.

The current code:

export = Ods;
export as namespace Ods;

declare namespace Ods {

  type IdType = 'Carta Identità' | 'Passaporto' | 'Patente'
  
  type ODSModule = 'SCUOLA' | 'OPERATORI' | 'DRONI' | 'ODS_ROOT'

  // ...

  interface Address {
    street: string;
    city: string;
    province: string;
    CAP: string;
  }

  // ...
}

… should be replaced by:


export type IdType = 'Carta Identità' | 'Passaporto' | 'Patente'
  
export type ODSModule = 'SCUOLA' | 'OPERATORI' | 'DRONI' | 'ODS_ROOT'

// ...

export interface Address {
  street: string;
  city: string;
  province: string;
  CAP: string;
}

// ...

Then, the module can be imported as a namespace if you prefer this way:

import * as Ods from "@revodigital/suiteods-types";
Bergquist answered 8/3, 2021 at 16:43 Comment(2)
I've updated the file with "pure types" but still the same problem... I'm updating the question in order to provide a clearer picture of the issueShorts
@LeonardoViada I edited about how to get rid of the namespace, let me know if it is clear. I see that you published your package, it is a valid solution. If you still want to keep your dependency as a private "devDependency", then you'll need to look into the compiled file dist/index.js in order to understand why the private package is imported: you could copy the line with the require("@revodigital/suiteods-types"), then the lines where the imported variable from the package is used.Bergquist

© 2022 - 2024 — McMap. All rights reserved.