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?
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
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; }'.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With