Skip to content

Instantly share code, notes, and snippets.

@aztack
Created September 23, 2025 09:16
Show Gist options
  • Save aztack/658eefee73ee4231c8102a84fce0c947 to your computer and use it in GitHub Desktop.
Save aztack/658eefee73ee4231c8102a84fce0c947 to your computer and use it in GitHub Desktop.
TypeScript Error Reporter
#!/usr/bin/env ts-node
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable curly */
/* eslint-disable @typescript-eslint/no-use-before-define */
/*
* Custom type-check runner
*
* Usage examples:
* npx ts-node scripts/custom-type-check.ts \
* --config packages/pivot-services/tsconfig.check-type.json \
* --exclude-path ../../.eden-mono/temp/node_modules \
* --exclude-path dist
*
* Flags:
* --config <path> Path to a tsconfig.json file (defaults to ./tsconfig.json)
* --exclude-path <path> Repeated flag – diagnostics whose fileName contains this
* substring will be dropped.
* --include-path <path> Repeated flag – if provided, only diagnostics whose
* fileName contains at least one of these substrings are kept.
*
* Exit status is 0 when no diagnostics remain after filtering, otherwise 1.
*/
import path from "path"
import ts from "typescript"
/* --------------------------------------------------------
* CLI argument parsing (minimal – avoids extra dependencies)
* -------------------------------------------------------- */
interface CliOptions {
configPath: string
exclude: string[]
include: string[]
excludeErrors: number[]
includeErrors: number[]
}
function parseArgs(argv: string[]): CliOptions {
const opts: CliOptions = {
configPath: "tsconfig.json",
exclude: [],
include: [],
excludeErrors: [],
includeErrors: [],
}
const takeValue = (i: number, arr: string[]): [string, number] => {
// Helper to support --flag=value or --flag value
const raw = arr[i]
if (!raw) throw new Error(`${raw} requires a value`)
const eq = raw.indexOf("=")
if (eq !== -1) {
return [raw.slice(eq + 1), i]
} else {
if (i + 1 >= arr.length) throw new Error(`${raw} requires a value`)
return [arr[i + 1]!, i + 1]
}
}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
if (!arg) continue
switch (true) {
case arg === "--config" || arg.startsWith("--config="): {
const [val, newIdx] = takeValue(i, argv)
opts.configPath = val
i = newIdx
break
}
case arg === "--exclude-path" || arg.startsWith("--exclude-path="): {
const [val, newIdx] = takeValue(i, argv)
opts.exclude.push(val)
i = newIdx
break
}
case arg === "--include-path" || arg.startsWith("--include-path="): {
const [val, newIdx] = takeValue(i, argv)
opts.include.push(val)
i = newIdx
break
}
case arg === "--exclude-errors" || arg.startsWith("--exclude-errors="): {
const [val, newIdx] = takeValue(i, argv)
opts.excludeErrors.push(
...val
.split(",")
.map((s) => Number(s.trim()))
.filter((n) => !Number.isNaN(n)),
)
i = newIdx
break
}
case arg === "--include-errors" || arg.startsWith("--include-errors="): {
const [val, newIdx] = takeValue(i, argv)
opts.includeErrors.push(
...val
.split(",")
.map((s) => Number(s.trim()))
.filter((n) => !Number.isNaN(n)),
)
i = newIdx
break
}
default: {
console.warn(`Unknown argument: ${arg}`)
}
}
}
return opts
}
/* --------------------------------------------------------
* Diagnostic filtering helpers
* -------------------------------------------------------- */
function shouldKeepDiagnostic(
d: ts.Diagnostic,
{
include,
exclude,
includeErrors,
excludeErrors,
}: {
include: string[]
exclude: string[]
includeErrors: number[]
excludeErrors: number[]
},
): boolean {
// Filter by error codes first
if (excludeErrors.includes(d.code)) return false
if (includeErrors.length > 0 && !includeErrors.includes(d.code)) return false
// Diagnostics without a specific file (e.g. config errors) are accepted after code filtering
if (!d.file) return true
const fileName = path.resolve(d.file.fileName)
// Exclude path filter (supports regex wrapped in / / as before)
const matchesExclude = exclude.some((ex) => {
if (ex.startsWith("/") && ex.endsWith("/")) {
const regex = new RegExp(ex.slice(1, -1))
return regex.test(fileName)
}
return fileName.includes(ex)
})
if (matchesExclude) return false
// Include path filter
if (include.length > 0 && !include.some((inc) => fileName.includes(inc))) return false
return true
}
/* --------------------------------------------------------
* Main execution flow
* -------------------------------------------------------- */
function run() {
const { configPath, exclude, include, excludeErrors, includeErrors } = parseArgs(process.argv.slice(2))
const resolvedConfigPath = path.resolve(configPath)
const configFile = ts.readConfigFile(resolvedConfigPath, ts.sys.readFile)
if (configFile.error) {
reportAndExit([configFile.error])
}
const parsedCmd = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(resolvedConfigPath),
/* existingOptions */ {},
resolvedConfigPath,
)
const program = ts.createProgram({
rootNames: parsedCmd.fileNames,
options: parsedCmd.options,
})
const diagnostics: ts.Diagnostic[] = [
...program.getOptionsDiagnostics(),
...program.getGlobalDiagnostics(),
...program.getSyntacticDiagnostics(),
...program.getSemanticDiagnostics(),
]
const filtered = diagnostics.filter((d) =>
shouldKeepDiagnostic(d, {
include,
exclude,
includeErrors,
excludeErrors,
}),
)
reportAndExit(filtered)
}
function reportAndExit(diags: ts.Diagnostic[]): void {
const formatHost: ts.FormatDiagnosticsHost = {
getCanonicalFileName: (f) => f,
getCurrentDirectory: ts.sys.getCurrentDirectory,
getNewLine: () => ts.sys.newLine,
}
if (diags.length > 0) {
console.error(ts.formatDiagnosticsWithColorAndContext(diags, formatHost))
console.error(`\nType checking failed with ${diags.length} error(s).`)
process.exit(1)
} else {
console.log("Type checking passed with no errors.")
}
}
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment