Skip to content

Commit 811a010

Browse files
committed
feat: improve component event handler type
1 parent 8d6eefe commit 811a010

21 files changed

+10559
-170
lines changed

.changeset/clean-taxis-rule.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: improve component event handler type

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@
9898
"semver": "^7.3.5",
9999
"string-replace-loader": "^3.0.3",
100100
"svelte": "^3.57.0",
101+
"svelte2tsx": "^0.6.11",
101102
"typescript": "~5.0.0",
103+
"typescript-eslint-parser-for-extra-files": "^0.3.0",
102104
"vue-eslint-parser": "^9.0.0"
103105
},
104106
"publishConfig": {

src/parser/converts/attr.ts

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -346,52 +346,86 @@ function buildEventHandlerType(
346346
elementName: string,
347347
eventName: string
348348
) {
349-
const nativeEventHandlerType = [
350-
`(e:`,
351-
/**/ `'${eventName}' extends infer EVT`,
352-
/**/ /**/ `?EVT extends keyof HTMLElementEventMap`,
353-
/**/ /**/ /**/ `?HTMLElementEventMap[EVT]`,
354-
/**/ /**/ /**/ `:CustomEvent<any>`,
355-
/**/ /**/ `:never`,
356-
`)=>void`,
357-
].join("");
349+
const nativeEventHandlerType = `(e:${conditional({
350+
check: `'${eventName}'`,
351+
extends: `infer EVT`,
352+
true: conditional({
353+
check: `EVT`,
354+
extends: keyof(`HTMLElementEventMap`),
355+
true: `HTMLElementEventMap[EVT]`,
356+
false: `CustomEvent<any>`,
357+
}),
358+
false: `never`,
359+
})})=>void`;
358360
if (element.type !== "SvelteElement") {
359361
return nativeEventHandlerType;
360362
}
361363
if (element.kind === "component") {
362-
// `@typescript-eslint/parser` currently cannot parse `*.svelte` import types correctly.
363-
// So if we try to do a correct type parsing, it's argument type will be `any`.
364-
// A workaround is to inject the type directly, as `CustomEvent<any>` is better than `any`.
365-
366-
// const componentEvents = `import('svelte').ComponentEvents<${elementName}>`;
367-
// return `(e:'${eventName}' extends keyof ${componentEvents}?${componentEvents}['${eventName}']:CustomEvent<any>)=>void`;
368-
369-
return `(e:CustomEvent<any>)=>void`;
364+
const componentEventsType = `import('svelte').ComponentEvents<${elementName}>`;
365+
return `(e:${conditional({
366+
check: `0`,
367+
extends: `(1 & ${componentEventsType})`,
368+
// `componentEventsType` is `any`
369+
// `@typescript-eslint/parser` currently cannot parse `*.svelte` import types correctly.
370+
// So if we try to do a correct type parsing, it's argument type will be `any`.
371+
// A workaround is to inject the type directly, as `CustomEvent<any>` is better than `any`.
372+
true: `CustomEvent<any>`,
373+
// `componentEventsType` has an exact type.
374+
false: conditional({
375+
check: `'${eventName}'`,
376+
extends: `infer EVT`,
377+
true: conditional({
378+
check: `EVT`,
379+
extends: keyof(componentEventsType),
380+
true: `${componentEventsType}[EVT]`,
381+
false: `CustomEvent<any>`,
382+
}),
383+
false: `never`,
384+
}),
385+
})})=>void`;
370386
}
371387
if (element.kind === "special") {
372388
if (elementName === "svelte:component") return `(e:CustomEvent<any>)=>void`;
373389
return nativeEventHandlerType;
374390
}
375391
const attrName = `on:${eventName}`;
376-
const importSvelteHTMLElements =
377-
"import('svelte/elements').SvelteHTMLElements";
378-
return [
379-
`'${elementName}' extends infer EL`,
380-
/**/ `?(`,
381-
/**/ /**/ `EL extends keyof ${importSvelteHTMLElements}`,
382-
/**/ /**/ `?(`,
383-
/**/ /**/ /**/ `'${attrName}' extends infer ATTR`,
384-
/**/ /**/ /**/ `?(`,
385-
/**/ /**/ /**/ /**/ `ATTR extends keyof ${importSvelteHTMLElements}[EL]`,
386-
/**/ /**/ /**/ /**/ /**/ `?${importSvelteHTMLElements}[EL][ATTR]`,
387-
/**/ /**/ /**/ /**/ /**/ `:${nativeEventHandlerType}`,
388-
/**/ /**/ /**/ `)`,
389-
/**/ /**/ /**/ `:never`,
390-
/**/ /**/ `)`,
391-
/**/ /**/ `:${nativeEventHandlerType}`,
392-
/**/ `)`,
393-
/**/ `:never`,
394-
].join("");
392+
const svelteHTMLElementsType = "import('svelte/elements').SvelteHTMLElements";
393+
return conditional({
394+
check: `'${elementName}'`,
395+
extends: "infer EL",
396+
true: conditional({
397+
check: `EL`,
398+
extends: keyof(`${svelteHTMLElementsType}`),
399+
true: conditional({
400+
check: `'${attrName}'`,
401+
extends: "infer ATTR",
402+
true: conditional({
403+
check: `ATTR`,
404+
extends: keyof(`${svelteHTMLElementsType}[EL]`),
405+
true: `${svelteHTMLElementsType}[EL][ATTR]`,
406+
false: nativeEventHandlerType,
407+
}),
408+
false: `never`,
409+
}),
410+
false: nativeEventHandlerType,
411+
}),
412+
false: `never`,
413+
});
414+
415+
/** Generate `keyof T` type. */
416+
function keyof(type: string) {
417+
return `keyof ${type}`;
418+
}
419+
420+
/** Generate `C extends E ? T : F` type. */
421+
function conditional(types: {
422+
check: string;
423+
extends: string;
424+
true: string;
425+
false: string;
426+
}) {
427+
return `${types.check} extends ${types.extends}?(${types.true}):(${types.false})`;
428+
}
395429
}
396430

397431
/** Convert for Class Directive */

tests/fixtures/parser/ast/ts-event05-input.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
</script>
44

55
<Component on:foo="{e=>{
6-
// TODO: e.detail is number
6+
// e.detail is number
7+
// `@typescript-eslint/parser` doesn't get the correct types.
8+
// Using `typescript-eslint-parser-for-extra-files` will give we the correct types.
9+
// See `ts-event06-input.svelte` test case
710
e.detail;
811
}}" />

tests/fixtures/parser/ast/ts-event05-no-unused-expressions-result.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"ruleId": "no-unused-expressions",
44
"code": "e.detail;",
5-
"line": 7,
5+
"line": 10,
66
"column": 5
77
}
88
]

0 commit comments

Comments
 (0)