Skip to content

Instantly share code, notes, and snippets.

@chriseppstein
Last active March 28, 2024 04:05
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseppstein/ca5b0c7207f6b5f5322c6e5cb888e4b1 to your computer and use it in GitHub Desktop.
Save chriseppstein/ca5b0c7207f6b5f5322c6e5cb888e4b1 to your computer and use it in GitHub Desktop.
If you keep getting "Two different types with this name exist, but they are unrelated." from TypeScript, here's what you need to do to fix it.

Two different types with this name exist, but they are unrelated.

I'm a little slow, so maybe everyone else already realized this, but after struggling for almost a year with periodic "Two different types with this name exist" error from typescript, I've realized it is actually really helpful error and it highlights a real problem that I've failed to understand until this week.

I've been working around this issue by making sure that my libraries with common dependencies are always on the same version for some of our shared library dependencies. This works because package managers dedup the same version of a dependency. But when the versions diverge you get different instances of the library. In this mode, Good Guy TypeScript is telling us "Hey you're comparing values and types from different instances of the same library and that's a Bad Idea."

I've google this error a bunch of times, but somehow I've never found the advice I always needed, so I hope this write-up can be that for someone else.

Using Values from a Transitive Dependency in a Public API

If a library returns values from a dependency, there are subtle ways this can go wrong in JavaScript. If all you're doing is calling methods and properties on a returned value from a dependency, TypeScript can effectively validate that code against the correct version of that value's type from the types of the transitive dependency. But when you you want to pass that value along to another function or method in your code, things can quickly go wrong if you're not careful.

If your project has noImplicitAny enabled (and it should, because any is usually just a bug waiting to happen), you'll need to declare the parameter type. Like me, you probably just added that dependency to your project, imported it, and declared the parameter type that matched the value returned by the API and this all worked just fine because you've got the same dependency version. But, you're code is accidentally working right now and depending on the types involved, you've just introduced a compilation error waiting to happen.

If the types involved are just interfaces, you're going to be ok, because even though typescript sees two different types, it will calculate that they are assignable to each other and happily move along. But for concrete types like classes, enums, and some constants, they cannot be used interchangeably and a compilation error is warranted.

How to handle types of transitive dependencies

Consider this code:

import { doSomethingHard, HardResult } from 'myDependency';
import * as sharedDep from 'transitive-dependency';

export function doSomethingReallyHard(): boolean {
  let hardResult: HardResult = doSomethingHard();
  let transformed = transformResultInfo(hardResult.info);
  return transformed.successful;
}

function transformResultInfo(info: sharedDep.Information) {
  return { successful: info.wasSuccessful() && info.warnings.length === 0 };
}

If sharedDep.Information is a class, typescript is going to throw a fit if the versions aren't exactly the same. Even a patch-level difference is a bug. You may be thinking "but semver says this is correctly typed" and, yes, but if it didn't give you an error, maybe you'd try doing something like info instanceof sharedDep.Information inside the function and that would return false unexpectedly. If it were a typescript enum the assigned constant values for the same enum identifier could wind up different after adding a new entry. (Yes, technically such a change requires a semver major version bump but it's really easy to miss this, especially if you like to alphabetize your enums.) Let's walk through some ways to write this code that prevents us from ever seeing the "Two different types" error.

Library Consumer: Use Type Alias to Property Accessor Types

We can use type aliases to make this work as versions are updated:

import { doSomethingHard, HardResult } from 'myDependency';
// Remove this import wherever we're working with the values returned by our direct dependency.
// import * as sharedDep from 'transitive-dependency';


// Local Type alias inferred from our direct dependency
type ResultInfo = HardResult['info'];

export function doSomethingReallyHard(): boolean {
  let hardResult: HardResult = doSomethingHard();
  let transformed = checkResultInfo(hardResult.info);
  return transformed.successful;
}

// You don't have to make an alias, especially if this function is exported it may be altogether easier to
// directly set the parameter's type to HardResult['info'].
function checkResultInfo(info: ResultInfo) {
  return { successful: info.wasSuccessful() && info.warnings.length === 0 };
}

Library Consumer: Define a Minimal Interface

We may need to work with values returned by our app's direct dependency on as well as on the values returned from the transitive dependency which may be out of sync with our version. In this case, it's not sufficient to use an alias to avoid compiler errors if the versions are out of sync.

Even if the value returned is not a class, it may itself be a complex interface and have properties with types that aren't friendly to our multiple installs of the library. And when we expect the full type, even if it's an interface, we can find ourselves facing an annoying compilation error when the type changes by adding a property in some way that's not relevant to our own code.

One of my favorite things about TypeScript is how it is fundamentally built upon a 1st class notion of "duck types" AKA interfaces. We can navigate this issue by first defining a minimal interface that only has the values we care about in it.

import { doSomethingHard, HardResult } from 'myDependency';
// Remove this import wherever we're working with the values returned by our direct dependency.
// import * as sharedDep from 'transitive-dependency';

// Minimal definition of what we need to use
export interface ResultInfo {
  wasSuccessful(): boolean;
  warnings: Array<string>;
}

export function doSomethingReallyHard(): boolean {
  let hardResult: HardResult = doSomethingHard();
  let transformed = checkResultInfo(hardResult.info);
  return transformed.successful;
}

// This is exported because sometimes we need to use this on our app's necessary direct usage
// of the shared transitive dependency.
export function checkResultInfo(info: ResultInfo) {
  return { successful: info.wasSuccessful() && info.warnings.length === 0 };
}

In this case, we are writing code that works with any version of the shared dependency as long as all the versions we are using have a type that is assignable to our minimal interface.

Now, this isn't very DRY, but in all the cases where the duplication causes issues, it's catching an error that we need to handle somehow. Having a type to mediate the two is likely going to be helpful. But, if it offends you, TypeScript has a convenient way to define a subset of a type called a Pick:

import { doSomethingHard, HardResult } from 'myDependency';
import { Information } as sharedDep from 'transitive-dependency';

// Minimal definition of what we need to use, the source type can be an interface or a class.
export type ResultInfo = Pick<Information, 'wasSuccessful' | 'warnings'>;

export function doSomethingReallyHard(): boolean {
  let hardResult: HardResult = doSomethingHard();
  let transformed = checkResultInfo(hardResult.info);
  return transformed.successful;
}

// This is exported because sometimes we need to use this on our app's necessary direct usage
// of the shared transitive dependency.
export function checkResultInfo(info: ResultInfo) {
  return { successful: info.wasSuccessful() && info.warnings.length === 0 };
}

As a library consumer, that's two good ways along with some small variations on each to handle working with types from transitive dependencies without getting the "Two different types with this name exist" error.

Library Authoring

You may very well be a library author, and building your code so that your library consumers have to understand this nuance of typescript is not the greatest idea. They probably don't follow me on twitter and so they haven't read this gist. So what can you do to keep this issue from affecting your users?

Library Author: Don't expose public values with a type from your dependencies.

The best solution to this issue is to not expose any public values that have a type that is specified from your dependency.

import * as transitiveDep from 'transitive-dependency';
export type ResultInfo = Pick<Information, 'wasSuccessful' | 'warnings'>;

// Minimal definition of what we expect our api consumers to use from our dependency.
export interface ResultInfo {
  wasSuccessful(): boolean;
  warnings: Array<string>;
}

export function doSomethingHard(): ResultInfo {
  let warnings = new Array<string>();
  // do hard stuff
  return new transitiveDep.Information(true, warnings, null, 42);
}

Note that we're still returning a value from our dependency so there's no performance hit, but by specifying our own type, we can control how that type changes over time. If we need to take a major version upgrade of our dependency, we can do so, and provide a shim that insulates our users from any changes in a dependency from affecting our own published API.

Even though it's verbose and feels redundant, I really like writing out the full interface here. Using an alias can end up with changes to your public API that are not obvious and may not even have compilation or test failures, but end up exposing a type that isn't yours to your consumers. Still, if you hate the idea of basically copy and pasting type definitions to your code here's some alternatives:

  • Just an alias export type ResultInfo = transitiveDep.Information; - This is my least favorite, but it does, at least, afford you the opportunity to change the definition of ResultInfo as you take an upgrade to transitive-dependency.
  • Pick the hard stuff, copy the easy stuff - Some types have really complex function overloads, you can define an interface for the straightforward parts and combine it with a Pick for the complex properties: export type ResultInfo = { warnings: Array<string>; } & Pick<transitiveDep.Information, 'wasSuccessful'>;
  • Purposefully exclude some properties that you don't want to expose (requires TypeScript 2.8+): export type ResultInfo = Pick<transitiveDep.Information, Exclude<keyof transitiveDep.Information, "newMethod">>;

Library Author: Use an opaque pass-through

Sometimes you just need to give your user a value so that later than can give it back to you. In this case, you can pass an Opaque value. The simplest definition of an opaque is {}. The goal is to pass it to your user without exposing any unnecessary attributes, keeping your public API very minimal. Then, when you get it back, you cast it to the full type value that belongs to your dependency. In some cases when working with a type hierarchy, you will find that including a discriminator field in your opaque can make working with it much cleaner. For example, if you need to pass a postcss Node:

// This definition works because Node is a union of all node types in postcss.
type OpaqueNode = Pick<postcss.Node, "type">;
export function getRule(): OpaqueNode {
  return postcss.rule();
}
function checkRule(o: OpaqueNode) {
  if (o.type !== "rule") { throw new ArgumentError("expected a rule"); }
  let rule = <postcss.Rule>o;
}

Library Author: Expose your public transitive dependencies.

Some transitive dependencies are just so intertwined with your library that it's not possible or desired to obscure it from your public API. In this case, you should export that dependency as part of your public API and make a note in your API documentation that consumers should use the exported dependency where they are interacting with your library.

// index.ts
import * as transitiveDependency from 'transitive-dependency';
export * from "./doSomethingHard";
export {
  transitiveDependency
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment