Skip to content

Commit 2f91db3

Browse files
committed
feat(sfc): support using declared interface or type alias with defineProps()
1 parent d069796 commit 2f91db3

File tree

3 files changed

+175
-8
lines changed

3 files changed

+175
-8
lines changed

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

+76
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,63 @@ return { emit }
780780
})"
781781
`;
782782
783+
exports[`SFC compile <script setup> with TypeScript defineProps w/ exported interface 1`] = `
784+
"import { defineComponent as _defineComponent } from 'vue'
785+
export interface Props { x?: number }
786+
787+
export default _defineComponent({
788+
props: {
789+
x: { type: Number, required: false }
790+
} as unknown as undefined,
791+
setup(__props: { x?: number }, { expose }) {
792+
expose()
793+
794+
795+
796+
return { }
797+
}
798+
799+
})"
800+
`;
801+
802+
exports[`SFC compile <script setup> with TypeScript defineProps w/ exported type alias 1`] = `
803+
"import { defineComponent as _defineComponent } from 'vue'
804+
export type Props = { x?: number }
805+
806+
export default _defineComponent({
807+
props: {
808+
x: { type: Number, required: false }
809+
} as unknown as undefined,
810+
setup(__props: { x?: number }, { expose }) {
811+
expose()
812+
813+
814+
815+
return { }
816+
}
817+
818+
})"
819+
`;
820+
821+
exports[`SFC compile <script setup> with TypeScript defineProps w/ interface 1`] = `
822+
"import { defineComponent as _defineComponent } from 'vue'
823+
interface Props { x?: number }
824+
825+
export default _defineComponent({
826+
props: {
827+
x: { type: Number, required: false }
828+
} as unknown as undefined,
829+
setup(__props: { x?: number }, { expose }) {
830+
expose()
831+
832+
833+
834+
return { }
835+
}
836+
837+
})"
838+
`;
839+
783840
exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = `
784841
"import { defineComponent as _defineComponent } from 'vue'
785842
@@ -840,6 +897,25 @@ export default _defineComponent({
840897
841898
842899
900+
return { }
901+
}
902+
903+
})"
904+
`;
905+
906+
exports[`SFC compile <script setup> with TypeScript defineProps w/ type alias 1`] = `
907+
"import { defineComponent as _defineComponent } from 'vue'
908+
type Props = { x?: number }
909+
910+
export default _defineComponent({
911+
props: {
912+
x: { type: Number, required: false }
913+
} as unknown as undefined,
914+
setup(__props: { x?: number }, { expose }) {
915+
expose()
916+
917+
918+
843919
return { }
844920
}
845921

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

+56
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,62 @@ const emit = defineEmits(['a', 'b'])
592592
})
593593
})
594594

595+
test('defineProps w/ interface', () => {
596+
const { content, bindings } = compile(`
597+
<script setup lang="ts">
598+
interface Props { x?: number }
599+
defineProps<Props>()
600+
</script>
601+
`)
602+
assertCode(content)
603+
expect(content).toMatch(`x: { type: Number, required: false }`)
604+
expect(bindings).toStrictEqual({
605+
x: BindingTypes.PROPS
606+
})
607+
})
608+
609+
test('defineProps w/ exported interface', () => {
610+
const { content, bindings } = compile(`
611+
<script setup lang="ts">
612+
export interface Props { x?: number }
613+
defineProps<Props>()
614+
</script>
615+
`)
616+
assertCode(content)
617+
expect(content).toMatch(`x: { type: Number, required: false }`)
618+
expect(bindings).toStrictEqual({
619+
x: BindingTypes.PROPS
620+
})
621+
})
622+
623+
test('defineProps w/ type alias', () => {
624+
const { content, bindings } = compile(`
625+
<script setup lang="ts">
626+
type Props = { x?: number }
627+
defineProps<Props>()
628+
</script>
629+
`)
630+
assertCode(content)
631+
expect(content).toMatch(`x: { type: Number, required: false }`)
632+
expect(bindings).toStrictEqual({
633+
x: BindingTypes.PROPS
634+
})
635+
})
636+
637+
test('defineProps w/ exported type alias', () => {
638+
const { content, bindings } = compile(`
639+
<script setup lang="ts">
640+
export type Props = { x?: number }
641+
defineProps<Props>()
642+
</script>
643+
`)
644+
assertCode(content)
645+
expect(content).toMatch(`x: { type: Number, required: false }`)
646+
expect(bindings).toStrictEqual({
647+
x: BindingTypes.PROPS
648+
})
649+
})
650+
595651
test('withDefaults (static)', () => {
596652
const { content, bindings } = compile(`
597653
<script setup lang="ts">

packages/compiler-sfc/src/compileScript.ts

+43-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
Expression,
2222
LabeledStatement,
2323
CallExpression,
24-
RestElement
24+
RestElement,
25+
TSInterfaceBody
2526
} from '@babel/types'
2627
import { walk } from 'estree-walker'
2728
import { RawSourceMap } from 'source-map'
@@ -195,7 +196,7 @@ export function compileScript(
195196
let hasDefineExposeCall = false
196197
let propsRuntimeDecl: Node | undefined
197198
let propsRuntimeDefaults: Node | undefined
198-
let propsTypeDecl: TSTypeLiteral | undefined
199+
let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
199200
let propsIdentifier: string | undefined
200201
let emitRuntimeDecl: Node | undefined
201202
let emitTypeDecl: TSFunctionType | TSTypeLiteral | undefined
@@ -287,12 +288,46 @@ export function compileScript(
287288
)
288289
}
289290

290-
const typeArg = node.typeParameters.params[0]
291+
let typeArg: Node = node.typeParameters.params[0]
291292
if (typeArg.type === 'TSTypeLiteral') {
292293
propsTypeDecl = typeArg
293-
} else {
294+
} else if (
295+
typeArg.type === 'TSTypeReference' &&
296+
typeArg.typeName.type === 'Identifier'
297+
) {
298+
const refName = typeArg.typeName.name
299+
const isValidType = (node: Node): boolean => {
300+
if (
301+
node.type === 'TSInterfaceDeclaration' &&
302+
node.id.name === refName
303+
) {
304+
propsTypeDecl = node.body
305+
return true
306+
} else if (
307+
node.type === 'TSTypeAliasDeclaration' &&
308+
node.id.name === refName &&
309+
node.typeAnnotation.type === 'TSTypeLiteral'
310+
) {
311+
propsTypeDecl = node.typeAnnotation
312+
return true
313+
} else if (
314+
node.type === 'ExportNamedDeclaration' &&
315+
node.declaration
316+
) {
317+
return isValidType(node.declaration)
318+
}
319+
return false
320+
}
321+
322+
for (const node of scriptSetupAst) {
323+
if (isValidType(node)) break
324+
}
325+
}
326+
327+
if (!propsTypeDecl) {
294328
error(
295-
`type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
329+
`type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
330+
`or a reference to a interface or literal type.`,
296331
typeArg
297332
)
298333
}
@@ -661,7 +696,6 @@ export function compileScript(
661696
for (const node of scriptSetupAst) {
662697
const start = node.start! + startOffset
663698
let end = node.end! + startOffset
664-
// import or type declarations: move to top
665699
// locate comment
666700
if (node.trailingComments && node.trailingComments.length > 0) {
667701
const lastCommentNode =
@@ -1315,11 +1349,12 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
13151349
}
13161350

13171351
function extractRuntimeProps(
1318-
node: TSTypeLiteral,
1352+
node: TSTypeLiteral | TSInterfaceBody,
13191353
props: Record<string, PropTypeData>,
13201354
declaredTypes: Record<string, string[]>
13211355
) {
1322-
for (const m of node.members) {
1356+
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
1357+
for (const m of members) {
13231358
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
13241359
props[m.key.name] = {
13251360
key: m.key.name,

0 commit comments

Comments
 (0)