I am encountering an issue with the TypeScript type checker that I do not understand.
I have two files representing snapshots: newest.ts (the latest version) and older.ts (the previous version).
Content of older.ts:
export type A = number;
Content of newest.ts:
export type A = number | string;
My project uses the same TypeScript program and checker to process files as follows:
Project {
static async process(entrypoints: string | string[], options: ts.CompilerOptions, host: ts.ModuleResolutionHost): Promise<Project> {
if (!Array.isArray(entrypoints)) {
return this.process([entrypoints], options, host);
}
const program = ts.createProgram(entrypoints, options);
// Additional processing...
}
}
const compilerOptions: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
baseUrl: process.cwd(), // Adjust baseUrl to your project structure
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS,
strictNullChecks: true
};
const resolutionHost: ts.ModuleResolutionHost = {
fileExists: (fileName: string) => ts.sys.fileExists(fileName),
readFile: (fileName: string) => ts.sys.readFile(fileName),
directoryExists: (directoryName: string) => ts.sys.directoryExists(directoryName),
getCurrentDirectory: () => process.cwd(),
useCaseSensitiveFileNames: () => true
};
const pathNewest = Path.join(__dirname, "./resources/newest.ts"); // The newest file
const pathOlder = Path.join(__dirname, "./resources/older.ts"); // The older file
const project = await Project.process([pathNewest, pathOlder], compilerOptions, resolutionHost);
const newestFile = project.files.find(file => file.filename === pathNewest); // Extract the newest file
const olderFile = project.files.find(file => file.filename === pathOlder); // Extract the older file
const checker = project.program.getTypeChecker(); // Get the checker for processing
compare(checker, olderFile, newestFile); // Compare the two files
I implemented a comparison function inspired by your approach:
const compare = (checker: ts.TypeChecker, older: File | Exportable, newest: File | Exportable): UpdateType => {
if (older.isFile() && newest.isFile()) {
// File processing logic...
}
if (older.isType() && newest.isType()) {
// `older.source` & `newest.source` are `ts.TypeAliasDeclaration` extracted from the same program.
const olderType = checker.getApparentType(checker.getTypeAtLocation(older.source));
const newestType = checker.getApparentType(checker.getTypeAtLocation(newest.source));
// I also tried without using the apparent type:
// const olderType = checker.getTypeAtLocation(older.source);
// const newestType = checker.getTypeAtLocation(newest.source);
console.log(older.source.symbol.parent.escapedName); // OUTPUT: "<PROJECT_PATH>/resources/older"
console.log(newest.source.symbol.parent.escapedName); // OUTPUT: "<PROJECT_PATH>/resources/newest"
// Both types come from their respective newest & older file.
console.log(`${checker.typeToString(olderType)} --> ${checker.typeToString(newestType)} = ${checker.isTypeAssignableTo(olderType, newestType)}`);
// OUTPUT: A --> A = true
console.log(`${checker.typeToString(newestType)} --> ${checker.typeToString(olderType)} = ${checker.isTypeAssignableTo(newestType, olderType)}`);
// OUTPUT: A --> A = true (Unexpected: `string` should not be assignable to `number | string`)
if (checker.isTypeAssignableTo(olderType, newestType) && checker.isTypeAssignableTo(newestType, olderType)) {
return UpdateType.SAME; // Unexpected result
}
if (checker.isTypeAssignableTo(olderType, newestType)) {
return UpdateType.MINOR;
}
return UpdateType.MAJOR;
}
return UpdateType.MAJOR;
};
The problem I am facing is that the type checker incorrectly reports:
A --> A = true
A --> A = true
Even though the type has changed from number
to number | string
, the checker still considers both assignable in both directions. I expected the check from newestType
to olderType
to return false
, as string
is not assignable to number
.
Do you have any insights into why the checker might behave this way? Could there be a scope issue? Am I missing something in the setup that could lead to this behavior? Like cache or something?
Thanks for your help!