DEV Community

Yoriiis
Yoriiis

Posted on

Managing private TypeScript types: beyond DefinitelyTyped

TypeScript gives us strong typing and we often rely on DefinitelyTyped for public libraries types. But as soon as you load libraries via a CDN or work on multiple internal projects that don't ship their own types, things start to break down. Each project defines its own version, duplication appears, and TypeScript starts yelling at you.

In large organizations, this often happens: you want all projects to share the same types, but in reality, each team often reinvents its own.

The solution? A centralized internal repository of TypeScript types.


How types propagate: NPM vs CDN

When you install a package from NPM that ships its own types, everything works out of the box. But when you use a library via a CDN, TypeScript can't infer anything. You have to declare the types manually, often by extending Window.

That's when problems appear. One team declares a subset of methods, another team declares slightly different ones. Merge those projects and TypeScript won't know which definition to trust.

Loading a script via a CDN rather than via NPM depends on the use case. Sometimes the script does not exist on NPM, sometimes you want to prioritise a specific execution order.


The problem: conflicting or missing types

Take the example of the following script loaded via a CDN, in this case the Dailymotion Web SDK.

<script defer src="https://geo.dailymotion.com/player.js"></script>
Enter fullscreen mode Exit fullscreen mode

It exposes a global object window.dailymotion. In TypeScript you'd declare it like this according to your requirements:

declare global {
  interface Window {
    dailymotion: {
      createPlayer: (
        selectorId: string,
        options: {
          player: string;
          video: string;
        }
      ) => Promise<{
        on: (event: string, callback: () => void) => void;
        play: () => void;
      }>;
      events: {
        PLAYER_START: string;
        PLAYER_END: string;
      };
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Now imagine two projects that use the Dailymotion SDK and do not declare exactly the same type. If you merge the two, conflicts will arise. This is often unavoidable because each writes its own version, which may differ.

In the event of a conflict, the following TypeScript error may appear:

error TS2717: Subsequent property declarations must have the same type.

See details of error TS2717 on TypeScript.tv.


Centralizing types: the concept

The solution is to centralize all internal and CDN-related type definitions in a single monorepo. Think of it as your private DefinitelyTyped.

Benefits are immediate:

  • Consistency: all projects use the same definitions
  • Reusability: publish once, install everywhere from your private registry
  • Type safety: no more mismatched types between projects
  • Maintenance: fix a type once, all consumers get the update

At Prisma Media, we call our monorepo Prime-Types 🤖


Minimal implementation

A minimal monorepo structure could look like this:

package.json  // defines workspaces
types/
  dailymotion/
    package.json
    index.d.ts
    README.md
Enter fullscreen mode Exit fullscreen mode

The root package.json of the monorepo

{
  "name": "types",
  "workspaces": ["./types/*"],
  "scripts": {
    "dev": "tsc --noEmit --watch"
  },
  "devDependencies": {
    "typescript": "5.8.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Inside types/dailymotion/package.json:

{
  "name": "@prime-types/dailymotion",
  "version": "1.0.0",
  "description": "TypeScript types for Dailymotion",
  "exports": {
    ".": {
      "types": "./index.d.ts",
      "default": "./index.d.ts"
    }
  },
  "main": "./index.d.ts",
  "types": "./index.d.ts",
  "devDependencies": {
    "typescript": "5.8.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Example type definition for Dailymotion Web SDK in index.d.ts:

// /types/dailymotion/index.d.ts
declare global {
  interface Window {
    dailymotion: {
      createPlayer: (
        selectorId: string,
        options: {
          player: string;
          video: string;
        }
      ) => Promise<{
        on: (event: string, callback: () => void) => void;
        play: () => void;
      }>;
      events: {
        PLAYER_START: string;
        PLAYER_END: string;
      };
    };
  }
}

// Export to ensure the file is treated as a module
export {};
Enter fullscreen mode Exit fullscreen mode

💡 Add export {} at the end of the file if it does not export any types or if only a declare global exists.

To consume these types in a project, install the package in peer-dependencies preferably (see below why in this section).

npm install @prime-types/dailymotion
Enter fullscreen mode Exit fullscreen mode

By default, TypeScript searches for declarations in the node_modules/@types path and in your project source. The rest of node_modules is ignored. You must therefore add the path to your declaration files in the tsconfig.json to allow TypeScript to reference and propagate them.

{
  "include": [
    "./src/**/*",
+   "./node_modules/@prime-types"
  ]
}
Enter fullscreen mode Exit fullscreen mode

👀 Discover this example in the minimalist monorepo Prime Types on GitHub.


Managing multiple versions: peer dependencies

One pitfall may arise: multiple versions of the same type package.

If library A and library B depend on different versions of @prime-types/dailymotion, TypeScript errors may appear.

The solution is to always declare the type package as a peer dependency:

{
  "name": "my-website",
  "peerDependencies": {
    "@prime-types/dailymotion": "1.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

That way, only one version can be installed at the top level, and all projects align.


Alternative: a single package

For small teams, you may consider a simplified version: group all types into a single package.

Pros:

  • Trivial to set up and deploy
  • Install one package and you're done

Cons:

  • Harder to version types independently
  • Package size grows quickly as you add other types

A monorepo with multiple packages is more scalable in the long term.


Conclusion

DefinitelyTyped is ideal for open source, but internal projects sometimes need their own solution. Centralizing TypeScript types has several advantages:

  • Removes duplication and conflicts
  • Improves type safety across projects
  • Gives you a single source of truth

The complete example is available on GitHub, so you can have fun with it! 🧑‍💻

We have not discussed deployment in this article, but I will detail our deployment workflow with GitLab in a future article.


Top comments (0)