})
})
+ test('defineProps w/ extends interface', () => {
+ const { content, bindings } = compile(`
+ <script lang="ts">
+ interface Foo { x?: number }
+ </script>
+ <script setup lang="ts">
+ interface Bar extends Foo { y?: number }
+ interface Props extends Bar {
+ z: number
+ y: string
+ }
+ defineProps<Props>()
+ </script>
+ `)
+ assertCode(content)
+ expect(content).toMatch(`z: { type: Number, required: true }`)
+ expect(content).toMatch(`y: { type: String, required: true }`)
+ expect(content).toMatch(`x: { type: Number, required: false }`)
+ expect(bindings).toStrictEqual({
+ x: BindingTypes.PROPS,
+ y: BindingTypes.PROPS,
+ z: BindingTypes.PROPS
+ })
+ })
+
test('defineProps w/ exported interface', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
CallExpression,
RestElement,
TSInterfaceBody,
+ TSTypeElement,
AwaitExpression,
Program,
ObjectMethod,
return true
}
+ function getAstBody(): Statement[] {
+ return scriptAst
+ ? [...scriptSetupAst.body, ...scriptAst.body]
+ : scriptSetupAst.body
+ }
+
+ function resolveExtendsType(
+ node: Node,
+ qualifier: (node: Node) => boolean,
+ cache: Array<Node> = []
+ ): Array<Node> {
+ if (node.type === 'TSInterfaceDeclaration' && node.extends) {
+ node.extends.forEach(extend => {
+ if (
+ extend.type === 'TSExpressionWithTypeArguments' &&
+ extend.expression.type === 'Identifier'
+ ) {
+ const body = getAstBody()
+ for (const node of body) {
+ const qualified = isQualifiedType(
+ node,
+ qualifier,
+ extend.expression.name
+ )
+ if (qualified) {
+ cache.push(qualified)
+ resolveExtendsType(node, qualifier, cache)
+ return cache
+ }
+ }
+ }
+ })
+ }
+ return cache
+ }
+
+ function isQualifiedType(
+ node: Node,
+ qualifier: (node: Node) => boolean,
+ refName: String
+ ): Node | undefined {
+ if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) {
+ return node.body
+ } else if (
+ node.type === 'TSTypeAliasDeclaration' &&
+ node.id.name === refName &&
+ qualifier(node.typeAnnotation)
+ ) {
+ return node.typeAnnotation
+ } else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
+ return isQualifiedType(node.declaration, qualifier, refName)
+ }
+ }
+
+ // filter all extends types to keep the override declaration
+ function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
+ extendsTypes.forEach(extend => {
+ const body = (extend as TSInterfaceBody).body
+ body.forEach(newBody => {
+ if (
+ newBody.type === 'TSPropertySignature' &&
+ newBody.key.type === 'Identifier'
+ ) {
+ const name = newBody.key.name
+ const hasOverride = bodies.some(
+ seenBody =>
+ seenBody.type === 'TSPropertySignature' &&
+ seenBody.key.type === 'Identifier' &&
+ seenBody.key.name === name
+ )
+ if (!hasOverride) bodies.push(newBody)
+ }
+ })
+ })
+ }
+
function resolveQualifiedType(
node: Node,
qualifier: (node: Node) => boolean
node.typeName.type === 'Identifier'
) {
const refName = node.typeName.name
- const isQualifiedType = (node: Node): Node | undefined => {
- if (
- node.type === 'TSInterfaceDeclaration' &&
- node.id.name === refName
- ) {
- return node.body
- } else if (
- node.type === 'TSTypeAliasDeclaration' &&
- node.id.name === refName &&
- qualifier(node.typeAnnotation)
- ) {
- return node.typeAnnotation
- } else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
- return isQualifiedType(node.declaration)
- }
- }
- const body = scriptAst
- ? [...scriptSetupAst.body, ...scriptAst.body]
- : scriptSetupAst.body
+ const body = getAstBody()
for (const node of body) {
- const qualified = isQualifiedType(node)
+ let qualified = isQualifiedType(
+ node,
+ qualifier,
+ refName
+ ) as TSInterfaceBody
if (qualified) {
+ const extendsTypes = resolveExtendsType(node, qualifier)
+ if (extendsTypes.length) {
+ const bodies: TSTypeElement[] = [...qualified.body]
+ filterExtendsType(extendsTypes, bodies)
+ qualified.body = bodies
+ }
return qualified
}
}