chenglou/pretext · error · Error
Expected TypeScript consumer misuse to fail, but it compiled
Error message
Expected TypeScript consumer misuse to fail, but it compiled successfully.
What it means
smokeTypeScript() runs a deliberately invalid consumer program (`layout(prepared, '100', 20)` — passing a string for a number parameter) and asserts tsc rejects it. If tsc exits 0, this throws: it means the package's emitted type declarations are too permissive and no longer catch the misuse. This is a negative type-contract test for the public API.
Source
Thrown at scripts/package-smoke-test.ts:136
"const prepared = prepare('hello', '16px Inter')",
"const width = '100'",
'layout(prepared, width, 20)',
'',
].join('\n'),
)
const badCompile = run(
[path.join(root, 'node_modules', '.bin', tscBinaryName()), '-p', 'tsconfig.json'],
{
cwd: projectDir,
stdout: 'pipe',
stderr: 'pipe',
allowFailure: true,
},
)
if (badCompile.exitCode === 0) {
throw new Error('Expected TypeScript consumer misuse to fail, but it compiled successfully.')
}
const combinedOutput = `${badCompile.stdout}${badCompile.stderr}`
if (
!combinedOutput.includes("Argument of type 'string' is not assignable to parameter of type 'number'.") &&
!combinedOutput.includes("Type 'string' is not assignable to type 'number'.")
) {
throw new Error(`Unexpected TypeScript consumer error output:\n${combinedOutput}`)
}
console.log('ts ok')
}
async function createProject(dir: string, pkg: Record<string, unknown>): Promise<void> {
await mkdir(dir, { recursive: true })
await writeFile(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n')
}
View on GitHub (pinned to ac49b09b7d)
Solutions
- Inspect dist/layout.d.ts for the layout() signature and confirm the width parameter is typed exactly `number`.
- If the source type was widened, narrow it back in src/layout.ts and rebuild dist/.
- Re-run the smoke test to confirm the negative case is rejected again.
Example fix
// before — src/layout.ts widened the type export function layout(prepared: Prepared, width: string | number, lineHeight: number) // after export function layout(prepared: Prepared, width: number, lineHeight: number)
Defensive patterns
Strategy: validation
Validate before calling
// Before shipping, assert the negative type test fails as expected
import { spawnSync } from 'node:child_process'
const r = spawnSync('npx', ['tsc', '-p', 'tsconfig.negative.json'], { encoding: 'utf8' })
if (r.status === 0) throw new Error('Negative type test compiled — layout() width is not typed as number') Type guard
import type { Prepared } from '@chenglou/pretext'
// Compile-time guard: width MUST be number. If this assignment errors, the contract holds.
type WidthIsNumber = Parameters<typeof import('@chenglou/pretext').layout>[1] extends number ? true : never Prevention
- Keep layout()'s width parameter typed exactly `number` in src/layout.ts.
- Run the package smoke test (which includes the negative TS case) before every release.
When it happens
Trigger: The width parameter's type in the published dist/*.d.ts became `string | number`, `any`, or `unknown`, so passing '100' type-checks cleanly. Can also happen if the consumer's tsconfig relaxed strictness — but the smoke test pins strict:true, so the root cause is almost always the package's declarations.
Common situations: A refactor widened a parameter type (e.g. unioning in string for convenience), or a declaration-emission change dropped the precise number type.
Related errors
- Unexpected TypeScript consumer error output: ${combinedOutpu
- Expected exactly one tarball in ${packDir}, found ${tarballs
- Command failed (${result.exitCode}): ${cmd.join(' ')}
- Missing pre-wrap result for ${testCase.label}
- Invalid value for --${name}: ${raw}
AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12).
Data as JSON: /api/errors/3e440e513bc36f81.
Report an issue: GitHub.