-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Typescript Plugin
Incremental Migration for TypeScript
Seamlessly migrate your codebase to leverage the latest TypeScript compiler features while maintaining clarity, control, and CI stability.
π§ͺ Reference PR
π #902 β TypeScript Plugin PoC Implementation
Metric
TypeScript code quality based on compiler diagnostics.
Derived from diagnostic objects of category error
and warning
.
Property | Value | Description |
---|---|---|
value | 48 |
Total number of detected issues. |
displayValue | 48 errors |
Human-readable representation of the value. |
score | 0 |
Indicates whether the audit passed (1 ) or failed (0 ). |
User story
As a developer, I want to incrementally migrate my codebase to the latest TypeScript features while avoiding large-scale refactoring and CI failures.
The plugin should:
- Analyze relevant
Diagnostic
data. - Provide actionable feedback with precise line references.
- Help track regressions during migration.
Integration Requirements
Setup and Requirements
π¦ Package Dependencies
- Dependencies:
- typescript β β Required for diagnostics, already present in the project.
- Dev Dependencies: N/A
- Optional Dependencies: N/A
π Configuration Files
tsconfig.json
β Standard configuration file.tsconfig.next.json
β Migration-specific configuration.
Audit, groups and category maintenance
- π Audit: The audit slugs depending on the implementation could be a small set that is easy to maintain, or a TS-Code to slug manually maintained list.
- π Group: The groups are similar to audits easy depending approach. A convention aligning with compiler options might be hard. Some of the group slugs overlap with compiler options like
strict
which is a short hand option to enable all strict type checks. All those cases need to be maintained manually. - π Category: The categories could be the same as for EsLint plus one for the configuration linting.
Details maintenance
- π Issues: The details contain only issues, but those are a bit cumbersome to set up initially. The TypeScript's compiler diagnostics return diagnostic codes, and a subset of those codes are relevant for this plugin. The irrelevant ones are easy to filter out. The relevant ones need to manually be mapped to an audit in the maintained map.
- π Table: N/A
Runner maintenance
The runner is simple to maintain, consisting of only a few fully typed lines.
Acceptance criteria
- The target
tsconfig.json
serves as the primary source for report creation and determines the applied audits.- Parsed using helpers from the
typescript
package. - Default configuration is loaded based on the current
typescript
version. - Audits are derived from programmatically derived or a small set of static data.
- Audits are filtered by
onlyAudits
if specified.
- Parsed using helpers from the
- Issues are grouped by an audit slug.
-
docsUrl
points to the official documentation page. - Unsupported diagnostic codes are logged.
-
- Audits are organized into logically linked groups.
Implementation details
π Key Note: Thereβs no way to automatically link Diagnostic results to compiler options.
- There is no known way to link the Diagnostic results to compiler options or audits.
- Diagnostic results depend on both the active
tsconfig.json
configuration and TypeScript internal defaults. - TypeScript defaults are not exported. They are derived over internal logic that depends on the current package version.
- Ambiguity exists between disabled and missing compiler options.
- Ambiguity exists between configures and internally derived compiler options.
A draft implementation of the plugin can be found here: #902.
Setup
The plugin needs the following options:
type PluginOptions = { tsConfigPath?: string, onlyAudits?: AuditSlug[]} | undefined;
const pluginOptions: PluginOptions = {
tsConfigPath: `tsconfig.next.json`, // default is `tsconfig.json`
onlyAudits: [ 'no-implicite-any' ]
};
Generating TypeScript Diagnostics
The typescript
package exports a method to create a program. The required parameters are derived from a given tscinfig.json
file:
import { type Program, createProgram} from 'typescript';
const program:Program = createProgram(program);
The typescript
package exports a method to get the Diagnostic
results of a given program:
import { type Diagnostic, getPreEmitDiagnostics} from 'typescript';
const diagnostics:Diagnostic[] = getPreEmitDiagnostics(program);
All objects are identified by a unique code:
import { type Diagnostic } from 'typescript';
const diagnostic:Diagnostic = {
code: 2453,
message: '...',
category: 'error'
};
TypeScript diagnostic codes are grouped into ranges based on their source and purpose. Unfortunately this is not documented anywhere.
Here's how they are categorized:
Code Range | Type | Description |
---|---|---|
1XXX | Syntax Errors | Structural issues detected during parsing. |
2XXX | Semantic Errors | Type-checking and type-system violations. |
3XXX | Suggestions | Optional improvements (e.g., unused variables). |
4XXX | Declaration & Language Service | Used by editors (e.g., VSCode) for IntelliSense. |
5XXX | Internal Compiler Errors | Rare, unexpected failures in the compiler. |
6XXX | Configuration/Options Errors | Issues with tsconfig.json or compiler options. |
7XXX | noImplicitAny Errors | Issues with commandline compiler options. |
The diagnostic messages are exposed over a undocumented and undiscoverable const names Diagnostics
.
Additional information is derived from TypeScript's own guidelines on diagnostic code ranges
import { Diagnostics } from "typescript";
/**
* const Diagnostics = {
* Convert_to_default_import: {
* code: 95013,
* category: 3,
* key: 'Convert_to_default_import_95013',
* message: 'Convert to default import',
* reportsUnnecessary: undefined,
* elidedInCompatabilityPyramid: undefined,
* reportsDeprecated: undefined
* },
* };
* const [key, diagData] = codeToData(95013) // Convert_to_default_import_95013
*/
export function codeToData(searchCode: number) {
return Object.entries(Diagnostics as Record<string, { code: number }>).find(([_, {code}]) => code === searchCode);
}
This objects need to get linked to audits. Due to key note [1] we would have to maintain a manual map to link diagnostic errors to audits and groups. So far the only maintainable way to group diagnostics is to rely on the undocumented ranges.
export const TS_CODE_RANGE_NAMES = {
'1': 'syntax-errors',
'2': 'semantic-errors',
'4': 'language-service-errors',
'5': 'internal-errors',
'6': 'configuration-errors',
'9': 'unknown-codes',
} as const;
TypeScript Compiler Options
NOTE: This section is here to reflect learnings from the research
The existing compiler options configured in tsconfig.next.json
are different from the ones used in the program. This internal logic is not accessible and parts of that logic has to be duplicated in the plugin source, see key notes [2].
Case 1: Internally applied defaults options - key notes [5]
tsconfig.next.json
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
calculated compiler options inside the TS program
{
"compilerOptions": {
"verbatimModuleSyntax": true
"esModuleInterop": true
}
}
Solution 1
The npx tsc --init
command creates a tsconfig.json
file with all defaults listed as comments.
A suggested implementation could be npx -y tsc -p=typescript@{version} --init
.
generated tsconfig.json
file
This file gets parsed and stores as tsconfig.<version>.json
under node_modules/.code-pushup/typescript-plugin/ts-defaults
.
tsconfig.5.5.4.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
}
}
This data can get generated and loaded at runtime of the plugin and merge with the config retrieved form tsconfig.next.json
.
Solution 2 (favoured)
The generate login from above can install the current TypeScript version on the postinstall hook fired on every npm install
.
At runtime of the plugin can load this data and merge this defaults into the config retrieved form the given tsconfig.next.json
.
Case 2: : Internally expanding defaults/short-hand options - key notes [5]
tsconfig.next.json
{
"compilerOptions": {
"strict": true
}
}
calculated compiler options inside the TS program
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes" true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
}
}
Is TypeScript strict
acts as a short-hand and expands internally into multiple other options.
Solution 1
We can re-implement the internal logic to handle cases like strict
etc in the plugin source.