Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect breaking changes in Typescript

  1. We are looking for a solution to detect any braking change to types exported in .d.ts (like interface removal, changing property type, changing enum value, etc..). Do you know some tool for that?

  2. If there is no such thing, is there a way to parse exported items from .d.ts to some js/ts objects so I could do the breaking changes checking on my own?

I found this tool https://api-extractor.com/ that works with typings and with help of this https://www.npmjs.com/package/@microsoft/api-extractor-model. I tried some parsing with that and I was able to work with interfaces and enums but for others like variables and type aliases it doesn't provide much help so in the end, I would have to parse it from a plain string as declared in the .d.ts file.

Edit 1: Playground

like image 488
Kremdas Avatar asked Sep 15 '25 04:09

Kremdas


1 Answers

You can do this using a little type magic with typeof import(..), however it does not check for equality between types that are only exported. The reason for this is because typeof import(..) will only give us the types of values / functions / classes - not types / interfaces. Therefore, the only way to detect a breaking change from a type changing is if a value / function / class used that type. Of course, it wouldn't be a breaking change if you added an optional property, so we can simply use the extends on the two import types and check if the old version is assignable to the new version. Here is an example of a positive check:

declare module "my-lib" { // this can be the NPM version
  export const defaultPerson: {
    name: string;
    age: number;
  };
}

declare module "src/index.ts" { // this can be the live root of the current version
  export const defaultPerson: {
    name: string;
    age: number;
    newFeat?: string; // non-breaking
  };

  export const foo: string; // non-breaking
}

// this adds a compile-time constraint on the generics
type NestedAssignable<T, F extends T> = never;

// in a different npm project in root of "my-lib" that references the "my-lib" version from NPM
type NoBreakingChanges = NestedAssignable<
  Pick<typeof import("src/index.ts"), keyof typeof import("my-lib")>,
  typeof import("my-lib")
>;

TypeScript Playground Link

Here is an example of a failing case (the new prop in defaultPerson is no longer optional):

declare module "my-lib" { // this can be the NPM version
  export const defaultPerson: {
    name: string;
    age: number;
  };
}

declare module "src/index.ts" { // this can be the live root of the current version
  export const defaultPerson: {
    name: string;
    age: number;
    newFeat: string; // BREAKING
  };

  export const foo: string; // non-breaking
}

// this adds a compile-time constraint on the generics
type NestedAssignable<T, F extends T> = never;

// in a different npm project in root of "my-lib" that references the "my-lib" version from NPM
type NoBreakingChanges = NestedAssignable<
  Pick<typeof import("src/index.ts"), keyof typeof import("my-lib")>,
  typeof import("my-lib")
>;

TypeScript Playground Link

You can see it even gives nice error messages:

Type 'typeof import("my-lib")' does not satisfy the constraint 'Pick<typeof import("src/index.ts"), "defaultPerson">'.
  Types of property 'defaultPerson' are incompatible.
    Property 'newFeat' is missing in type '{ name: string; age: number; }' but required in type '{ name: string; age: number; newFeat: string; }'.
like image 61
sno2 Avatar answered Sep 17 '25 18:09

sno2