In VSCode if you have the appropriate settings enabled then the following .html
file will show you an error:
<!DOCTYPE html>
<html>
<body>
<div>
<select>
</select>
</div>
<script>
// @ts-check
/**
* @type {string}
*/
const data = 0;
</script>
</body>
</html>
However if you just add an empty onchange
:
<select onchange="">
Then the diagnostic disappears! You can still hover the variables and it shows you their correct types, but no diagnostics. Why is this?
I guess this is just how VS Code works. As found in comments, at the time of this writing, it seems that if you want JavaScript validation to happen, in addition to setting the html.validate.scripts
VS Code setting to true
, the first JavaScript code anywhere in the HTML document needs to start with a @ts-check
directive comment.
(skip to last paragraph for workaround)
This would support a hypothesis that what VS Code does is (probably with more nuance, but) pretty much concatenate all the JS code in the HTML document and then send that off to its JS-validation bits. I tried to dig into the VS Code source code to verify this, and I can sort of see it. You can trace through the following code:
if (textDocument.languageId === 'html') { const modes = languageModes.getAllModesInDocument(textDocument); const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation)); const latestTextDocument = documents.get(textDocument.uri); if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op for (const mode of modes) { if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) { pushAll(diagnostics, await mode.doValidation(latestTextDocument, settings)); } } return diagnostics; } }
https://github.com/microsoft/vscode/blob/9621add46007f7a1ab37d1fce9bcdcecca62aeb0/extensions/html-language-features/server/src/modes/javascriptMode.ts#L101 (particularly here):
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode { const jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId)); const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS); const globalSettings: Settings = {}; function updateHostSettings(settings: Settings) { const hostSettings = host.getCompilationSettings(); hostSettings.experimentalDecorators = settings?.['js/ts']?.implicitProjectConfig?.experimentalDecorators; hostSettings.strictNullChecks = settings?.['js/ts']?.implicitProjectConfig.strictNullChecks; } return { getId() { return languageId; }, async doValidation(document: TextDocument, settings = workspace.settings): Promise<Diagnostic[]> { updateHostSettings(settings); const jsDocument = jsDocuments.get(document); const languageService = await host.getLanguageService(jsDocument); const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(jsDocument.uri); const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri); return syntaxDiagnostics.concat(semanticDiagnostics).filter(d => !ignoredErrors.includes(d.code)).map((diag: ts.Diagnostic): Diagnostic => { return { range: convertRange(jsDocument, diag), severity: DiagnosticSeverity.Error, source: languageId, message: ts.flattenDiagnosticMessageText(diag.messageText, '\n') }; }); },
Especially since this question post was motivated by the addition of a script-valued attribute, what I find particularly interesting here is that if you look at the definition of getEmbeddedDocument
, it actually has an optional ignoreAttributeValues
parameter that isn't being used in the above call to it.
Anyhoo, it's not pretty, but if you want to use script-valued attributes and make sure you get JS checking, just try to put a script
element as early / as high up in the HTML file as possible with a @ts-check
directive comment in it. Ex. <script>//@ts-check</script>
near the top of the inside of your HTML document's head
element.
See also https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html#ts-check, which documents that for TypeScript to recognize a @ts-check
directive, it must be at the start of a JavaScript file.