Skip to content

Commit 83f7e6f

Browse files
authoredOct 26, 2022
fix(compiler-sfc): support using extends interface with defineProps() (#4512)
fix #4498
1 parent 183e4e6 commit 83f7e6f

File tree

3 files changed

+141
-20
lines changed

3 files changed

+141
-20
lines changed
 

‎packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap

+27
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,33 @@ export default /*#__PURE__*/_defineComponent({
14671467

14681468

14691469

1470+
return { }
1471+
}
1472+
1473+
})"
1474+
`;
1475+
1476+
exports[`SFC compile <script setup> with TypeScript defineProps w/ extends interface 1`] = `
1477+
"import { defineComponent as _defineComponent } from 'vue'
1478+
interface Bar extends Foo { y?: number }
1479+
interface Props extends Bar {
1480+
z: number
1481+
y: string
1482+
}
1483+
1484+
interface Foo { x?: number }
1485+
1486+
export default /*#__PURE__*/_defineComponent({
1487+
props: {
1488+
z: { type: Number, required: true },
1489+
y: { type: String, required: true },
1490+
x: { type: Number, required: false }
1491+
},
1492+
setup(__props: any, { expose }) {
1493+
expose()
1494+
1495+
1496+
14701497
return { }
14711498
}
14721499

‎packages/compiler-sfc/__tests__/compileScript.spec.ts

+25
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,31 @@ const emit = defineEmits(['a', 'b'])
885885
})
886886
})
887887

888+
test('defineProps w/ extends interface', () => {
889+
const { content, bindings } = compile(`
890+
<script lang="ts">
891+
interface Foo { x?: number }
892+
</script>
893+
<script setup lang="ts">
894+
interface Bar extends Foo { y?: number }
895+
interface Props extends Bar {
896+
z: number
897+
y: string
898+
}
899+
defineProps<Props>()
900+
</script>
901+
`)
902+
assertCode(content)
903+
expect(content).toMatch(`z: { type: Number, required: true }`)
904+
expect(content).toMatch(`y: { type: String, required: true }`)
905+
expect(content).toMatch(`x: { type: Number, required: false }`)
906+
expect(bindings).toStrictEqual({
907+
x: BindingTypes.PROPS,
908+
y: BindingTypes.PROPS,
909+
z: BindingTypes.PROPS
910+
})
911+
})
912+
888913
test('defineProps w/ exported interface', () => {
889914
const { content, bindings } = compile(`
890915
<script setup lang="ts">

‎packages/compiler-sfc/src/compileScript.ts

+89-20
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
CallExpression,
3737
RestElement,
3838
TSInterfaceBody,
39+
TSTypeElement,
3940
AwaitExpression,
4041
Program,
4142
ObjectMethod,
@@ -558,6 +559,82 @@ export function compileScript(
558559
return true
559560
}
560561

562+
function getAstBody(): Statement[] {
563+
return scriptAst
564+
? [...scriptSetupAst.body, ...scriptAst.body]
565+
: scriptSetupAst.body
566+
}
567+
568+
function resolveExtendsType(
569+
node: Node,
570+
qualifier: (node: Node) => boolean,
571+
cache: Array<Node> = []
572+
): Array<Node> {
573+
if (node.type === 'TSInterfaceDeclaration' && node.extends) {
574+
node.extends.forEach(extend => {
575+
if (
576+
extend.type === 'TSExpressionWithTypeArguments' &&
577+
extend.expression.type === 'Identifier'
578+
) {
579+
const body = getAstBody()
580+
for (const node of body) {
581+
const qualified = isQualifiedType(
582+
node,
583+
qualifier,
584+
extend.expression.name
585+
)
586+
if (qualified) {
587+
cache.push(qualified)
588+
resolveExtendsType(node, qualifier, cache)
589+
return cache
590+
}
591+
}
592+
}
593+
})
594+
}
595+
return cache
596+
}
597+
598+
function isQualifiedType(
599+
node: Node,
600+
qualifier: (node: Node) => boolean,
601+
refName: String
602+
): Node | undefined {
603+
if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) {
604+
return node.body
605+
} else if (
606+
node.type === 'TSTypeAliasDeclaration' &&
607+
node.id.name === refName &&
608+
qualifier(node.typeAnnotation)
609+
) {
610+
return node.typeAnnotation
611+
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
612+
return isQualifiedType(node.declaration, qualifier, refName)
613+
}
614+
}
615+
616+
// filter all extends types to keep the override declaration
617+
function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
618+
extendsTypes.forEach(extend => {
619+
const body = (extend as TSInterfaceBody).body
620+
body.forEach(newBody => {
621+
if (
622+
newBody.type === 'TSPropertySignature' &&
623+
newBody.key.type === 'Identifier'
624+
) {
625+
const name = newBody.key.name
626+
const hasOverride = bodies.some(
627+
seenBody =>
628+
seenBody.type === 'TSPropertySignature' &&
629+
seenBody.key.type === 'Identifier' &&
630+
seenBody.key.name === name
631+
)
632+
if (!hasOverride) bodies.push(newBody)
633+
}
634+
})
635+
})
636+
}
637+
561638
function resolveQualifiedType(
562639
node: Node,
563640
qualifier: (node: Node) => boolean
@@ -570,28 +647,20 @@ export function compileScript(
570647
node.typeName.type === 'Identifier'
571648
) {
572649
const refName = node.typeName.name
573-
const isQualifiedType = (node: Node): Node | undefined => {
574-
if (
575-
node.type === 'TSInterfaceDeclaration' &&
576-
node.id.name === refName
577-
) {
578-
return node.body
579-
} else if (
580-
node.type === 'TSTypeAliasDeclaration' &&
581-
node.id.name === refName &&
582-
qualifier(node.typeAnnotation)
583-
) {
584-
return node.typeAnnotation
585-
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
586-
return isQualifiedType(node.declaration)
587-
}
588-
}
589-
const body = scriptAst
590-
? [...scriptSetupAst.body, ...scriptAst.body]
591-
: scriptSetupAst.body
650+
const body = getAstBody()
592651
for (const node of body) {
593-
const qualified = isQualifiedType(node)
652+
let qualified = isQualifiedType(
653+
node,
654+
qualifier,
655+
refName
656+
) as TSInterfaceBody
594657
if (qualified) {
658+
const extendsTypes = resolveExtendsType(node, qualifier)
659+
if (extendsTypes.length) {
660+
const bodies: TSTypeElement[] = [...qualified.body]
661+
filterExtendsType(extendsTypes, bodies)
662+
qualified.body = bodies
663+
}
595664
return qualified
596665
}
597666
}

0 commit comments

Comments
 (0)
Please sign in to comment.