Skip to content

Commit 96a72a5

Browse files
authored
feat: improve component event handler type (#314)
* feat: improve component event handler type * fix: test * fix: test * chore: refactor
1 parent f13e307 commit 96a72a5

33 files changed

+10578
-206
lines changed

.changeset/clean-taxis-rule.md

+5
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

+2
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

+65-36
Original file line numberDiff line numberDiff line change
@@ -346,52 +346,81 @@ 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 `C extends E ? T : F` type. */
416+
function conditional(types: {
417+
check: string;
418+
extends: string;
419+
true: string;
420+
false: string;
421+
}) {
422+
return `${types.check} extends ${types.extends}?(${types.true}):(${types.false})`;
423+
}
395424
}
396425

397426
/** Convert for Class Directive */

tests/fixtures/integrations/parser-object-tests/ts-multiple-parser-setup.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
2-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
2+
import { generateParserOptions } from "../../../src/parser/test-utils";
33
import * as ts from "@typescript-eslint/parser";
44

55
export function getConfig() {
66
return {
77
parser: "svelte-eslint-parser",
8-
parserOptions: {
9-
...BASIC_PARSER_OPTIONS,
10-
parser: { ts },
11-
},
8+
parserOptions: generateParserOptions({ parser: { ts } }),
129
env: {
1310
browser: true,
1411
es2021: true,

tests/fixtures/integrations/parser-object-tests/ts-single-parser-setup.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
2-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
2+
import { generateParserOptions } from "../../../src/parser/test-utils";
33
import * as parser from "@typescript-eslint/parser";
44

55
export function getConfig() {
66
return {
77
parser: "svelte-eslint-parser",
8-
parserOptions: {
9-
...BASIC_PARSER_OPTIONS,
10-
parser,
11-
},
8+
parserOptions: generateParserOptions({ parser }),
129
env: {
1310
browser: true,
1411
es2021: true,

tests/fixtures/integrations/type-info-tests/await-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -12,7 +12,7 @@ export function setupLinter(linter: Linter) {
1212
export function getConfig() {
1313
return {
1414
parser: "svelte-eslint-parser",
15-
parserOptions: BASIC_PARSER_OPTIONS,
15+
parserOptions: generateParserOptions(),
1616
rules: {
1717
"@typescript-eslint/no-unsafe-call": "error",
1818
},

tests/fixtures/integrations/type-info-tests/i18n-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -12,7 +12,7 @@ export function setupLinter(linter: Linter) {
1212
export function getConfig() {
1313
return {
1414
parser: "svelte-eslint-parser",
15-
parserOptions: BASIC_PARSER_OPTIONS,
15+
parserOptions: generateParserOptions(),
1616
rules: {
1717
"@typescript-eslint/no-unsafe-call": "error",
1818
},

tests/fixtures/integrations/type-info-tests/issue226-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -12,7 +12,7 @@ export function setupLinter(linter: Linter) {
1212
export function getConfig() {
1313
return {
1414
parser: "svelte-eslint-parser",
15-
parserOptions: BASIC_PARSER_OPTIONS,
15+
parserOptions: generateParserOptions(),
1616
rules: {
1717
"@typescript-eslint/no-unsafe-argument": "error",
1818
},

tests/fixtures/integrations/type-info-tests/no-unnecessary-condition01-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -12,7 +12,7 @@ export function setupLinter(linter: Linter) {
1212
export function getConfig() {
1313
return {
1414
parser: "svelte-eslint-parser",
15-
parserOptions: BASIC_PARSER_OPTIONS,
15+
parserOptions: generateParserOptions(),
1616
rules: {
1717
"@typescript-eslint/no-unnecessary-condition": "error",
1818
},

tests/fixtures/integrations/type-info-tests/plugin-issue254-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -12,7 +12,7 @@ export function setupLinter(linter: Linter) {
1212
export function getConfig() {
1313
return {
1414
parser: "svelte-eslint-parser",
15-
parserOptions: BASIC_PARSER_OPTIONS,
15+
parserOptions: generateParserOptions(),
1616
rules: {
1717
"@typescript-eslint/no-unnecessary-condition": "error",
1818
},

tests/fixtures/integrations/type-info-tests/reactive-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -28,7 +28,7 @@ export function setupLinter(linter: Linter) {
2828
export function getConfig() {
2929
return {
3030
parser: "svelte-eslint-parser",
31-
parserOptions: BASIC_PARSER_OPTIONS,
31+
parserOptions: generateParserOptions(),
3232
rules: {
3333
"@typescript-eslint/no-unsafe-argument": "error",
3434
"@typescript-eslint/no-unsafe-assignment": "error",

tests/fixtures/integrations/type-info-tests/reactive2-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -16,7 +16,7 @@ export function setupLinter(linter: Linter) {
1616
export function getConfig() {
1717
return {
1818
parser: "svelte-eslint-parser",
19-
parserOptions: BASIC_PARSER_OPTIONS,
19+
parserOptions: generateParserOptions(),
2020
rules: {
2121
"@typescript-eslint/no-unsafe-assignment": "error",
2222
"@typescript-eslint/no-unsafe-member-access": "error",

tests/fixtures/integrations/type-info-tests/ts-newline-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -16,7 +16,7 @@ export function setupLinter(linter: Linter) {
1616
export function getConfig() {
1717
return {
1818
parser: "svelte-eslint-parser",
19-
parserOptions: BASIC_PARSER_OPTIONS,
19+
parserOptions: generateParserOptions(),
2020
rules: {
2121
"@typescript-eslint/no-confusing-void-expression": "error",
2222
"@typescript-eslint/explicit-function-return-type": "error",

tests/fixtures/integrations/type-info-tests/ts-no-misused-promises-setup.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint eslint-comments/require-description: 0, @typescript-eslint/explicit-module-boundary-types: 0 */
22
import type { Linter } from "eslint";
3-
import { BASIC_PARSER_OPTIONS } from "../../../src/parser/test-utils";
3+
import { generateParserOptions } from "../../../src/parser/test-utils";
44
import { rules } from "@typescript-eslint/eslint-plugin";
55
export function setupLinter(linter: Linter) {
66
linter.defineRule(
@@ -12,7 +12,7 @@ export function setupLinter(linter: Linter) {
1212
export function getConfig() {
1313
return {
1414
parser: "svelte-eslint-parser",
15-
parserOptions: BASIC_PARSER_OPTIONS,
15+
parserOptions: generateParserOptions(),
1616
rules: {
1717
"@typescript-eslint/no-misused-promises": "error",
1818
},

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

+4-1
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

+1-1
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)