Skip to content

Commit 9335bfd

Browse files
committed
feat(api): support TSMappedType
1 parent d944bb0 commit 9335bfd

File tree

5 files changed

+170
-52
lines changed

5 files changed

+170
-52
lines changed

.changeset/hot-bugs-ring.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@vue-macros/better-define': minor
3+
'@vue-macros/api': minor
4+
---
5+
6+
support union key (TSMappedType)

packages/api/src/ts.ts

Lines changed: 98 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import path from 'node:path'
44
import {
55
babelParse,
66
getFileCodeAndLang,
7+
isStaticExpression,
8+
resolveLiteral,
79
resolveObjectKey,
810
} from '@vue-macros/common'
911
import { isDeclaration } from '@babel/types'
@@ -21,6 +23,7 @@ import type {
2123
TSInterfaceBody,
2224
TSInterfaceDeclaration,
2325
TSIntersectionType,
26+
TSMappedType,
2427
TSMethodSignature,
2528
TSModuleBlock,
2629
TSModuleDeclaration,
@@ -30,6 +33,7 @@ import type {
3033
TSTypeAliasDeclaration,
3134
TSTypeElement,
3235
TSTypeLiteral,
36+
UnaryExpression,
3337
} from '@babel/types'
3438

3539
export type TSDeclaration =
@@ -57,7 +61,7 @@ export interface TSProperties {
5761
{
5862
value: TSResolvedType<TSType> | null
5963
optional: boolean
60-
signature: TSResolvedType<TSPropertySignature>
64+
signature: TSResolvedType<TSPropertySignature | TSMappedType>
6165
}
6266
>
6367
}
@@ -100,62 +104,108 @@ export async function resolveTSProperties({
100104
type,
101105
scope,
102106
}: TSResolvedType<
103-
TSInterfaceDeclaration | TSInterfaceBody | TSTypeLiteral | TSIntersectionType
107+
| TSInterfaceDeclaration
108+
| TSInterfaceBody
109+
| TSTypeLiteral
110+
| TSIntersectionType
111+
| TSMappedType
104112
>): Promise<TSProperties> {
105-
if (type.type === 'TSInterfaceBody') {
106-
return resolveTypeElements(scope, type.body)
107-
} else if (type.type === 'TSTypeLiteral') {
108-
return resolveTypeElements(scope, type.members)
109-
} else if (type.type === 'TSInterfaceDeclaration') {
110-
let properties = resolveTypeElements(scope, type.body.body)
111-
if (type.extends) {
112-
const resolvedExtends = (
113-
await Promise.all(
114-
type.extends.map((node) =>
115-
node.expression.type === 'Identifier'
116-
? resolveTSReferencedType({
117-
scope,
118-
type: node.expression,
119-
})
120-
: undefined
121-
)
122-
)
123-
)
124-
// eslint-disable-next-line unicorn/no-array-callback-reference
125-
.filter(filterValidExtends)
126-
127-
if (resolvedExtends.length > 0) {
128-
const ext = (
113+
switch (type.type) {
114+
case 'TSInterfaceBody':
115+
return resolveTypeElements(scope, type.body)
116+
case 'TSTypeLiteral':
117+
return resolveTypeElements(scope, type.members)
118+
case 'TSInterfaceDeclaration': {
119+
let properties = resolveTypeElements(scope, type.body.body)
120+
if (type.extends) {
121+
const resolvedExtends = (
129122
await Promise.all(
130-
resolvedExtends.map((resolved) => resolveTSProperties(resolved))
123+
type.extends.map((node) =>
124+
node.expression.type === 'Identifier'
125+
? resolveTSReferencedType({
126+
scope,
127+
type: node.expression,
128+
})
129+
: undefined
130+
)
131131
)
132-
).reduceRight((acc, curr) => mergeTSProperties(acc, curr))
133-
properties = mergeTSProperties(ext, properties)
132+
)
133+
// eslint-disable-next-line unicorn/no-array-callback-reference
134+
.filter(filterValidExtends)
135+
136+
if (resolvedExtends.length > 0) {
137+
const ext = (
138+
await Promise.all(
139+
resolvedExtends.map((resolved) => resolveTSProperties(resolved))
140+
)
141+
).reduceRight((acc, curr) => mergeTSProperties(acc, curr))
142+
properties = mergeTSProperties(ext, properties)
143+
}
134144
}
145+
return properties
135146
}
136-
return properties
137-
} else if (type.type === 'TSIntersectionType') {
138-
let properties: TSProperties = {
139-
callSignatures: [],
140-
constructSignatures: [],
141-
methods: {},
142-
properties: {},
147+
case 'TSIntersectionType': {
148+
let properties: TSProperties = {
149+
callSignatures: [],
150+
constructSignatures: [],
151+
methods: {},
152+
properties: {},
153+
}
154+
for (const subType of type.types) {
155+
const resolved = await resolveTSReferencedType({
156+
scope,
157+
type: subType,
158+
})
159+
if (!filterValidExtends(resolved)) continue
160+
properties = mergeTSProperties(
161+
properties,
162+
await resolveTSProperties(resolved)
163+
)
164+
}
165+
return properties
143166
}
144-
for (const subType of type.types) {
145-
const resolved = await resolveTSReferencedType({
167+
case 'TSMappedType': {
168+
const properties: TSProperties = {
169+
callSignatures: [],
170+
constructSignatures: [],
171+
methods: {},
172+
properties: {},
173+
}
174+
if (!type.typeParameter.constraint) return properties
175+
176+
const constraint = await resolveTSReferencedType({
177+
type: type.typeParameter.constraint,
146178
scope,
147-
type: subType,
148179
})
149-
if (!filterValidExtends(resolved)) continue
150-
properties = mergeTSProperties(
151-
properties,
152-
await resolveTSProperties(resolved)
153-
)
180+
if (!constraint?.type) return properties
181+
182+
const types =
183+
constraint.type.type === 'TSUnionType'
184+
? constraint.type.types
185+
: [constraint.type]
186+
187+
for (const subType of types) {
188+
if (subType.type !== 'TSLiteralType') continue
189+
const literal = subType.literal
190+
if (!isStaticExpression(literal)) continue
191+
const key = resolveLiteral(
192+
literal as Exclude<typeof literal, UnaryExpression>
193+
)
194+
if (!key) continue
195+
properties.properties[String(key)] = {
196+
value: type.typeAnnotation
197+
? { scope, type: type.typeAnnotation }
198+
: null,
199+
optional: type.optional === '+' || type.optional === true,
200+
signature: { type, scope },
201+
}
202+
}
203+
204+
return properties
154205
}
155-
return properties
156-
} else {
157-
// @ts-expect-error type is never
158-
throw new Error(`unknown node: ${type?.type}`)
206+
default:
207+
// @ts-expect-error type is never
208+
throw new Error(`unknown node: ${type?.type}`)
159209
}
160210

161211
function filterValidExtends(

packages/api/src/vue/props.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
StringLiteral,
2727
TSInterfaceDeclaration,
2828
TSIntersectionType,
29+
TSMappedType,
2930
TSMethodSignature,
3031
TSPropertySignature,
3132
TSType,
@@ -368,9 +369,12 @@ export async function handleTSPropsDefinition({
368369
}
369370
} else if (
370371
definitionsAst.type !== 'TSInterfaceDeclaration' &&
371-
definitionsAst.type !== 'TSTypeLiteral'
372+
definitionsAst.type !== 'TSTypeLiteral' &&
373+
definitionsAst.type !== 'TSMappedType'
372374
)
373-
throw new SyntaxError(`Cannot resolve TS definition.`)
375+
throw new SyntaxError(
376+
`Cannot resolve TS definition: ${definitionsAst.type}.`
377+
)
374378

375379
const properties = await resolveTSProperties({
376380
scope,
@@ -504,7 +508,7 @@ export interface TSPropsProperty {
504508
type: 'property'
505509
value: ASTDefinition<TSResolvedType['type']> | undefined
506510
optional: boolean
507-
signature: ASTDefinition<TSPropertySignature>
511+
signature: ASTDefinition<TSPropertySignature | TSMappedType>
508512

509513
/** Whether added by `addProp` API */
510514
addByAPI: boolean
@@ -521,7 +525,11 @@ export interface TSProps extends PropsBase {
521525

522526
definitions: Record<string | number, TSPropsMethod | TSPropsProperty>
523527
definitionsAst: ASTDefinition<
524-
TSInterfaceDeclaration | TSTypeLiteral | TSIntersectionType | TSUnionType
528+
| TSInterfaceDeclaration
529+
| TSTypeLiteral
530+
| TSIntersectionType
531+
| TSUnionType
532+
| TSMappedType
525533
>
526534

527535
/**

packages/better-define/tests/__snapshots__/fixtures.test.ts.snap

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,52 @@ export { unionEmits as default };
590590
"
591591
`;
592592

593+
exports[`fixtures > tests/fixtures/union-props.vue > isProduction = false 1`] = `
594+
"import { defineComponent } from 'vue';
595+
import _export_sfc from '/plugin-vue/export-helper';
596+
597+
var _sfc_main = /* @__PURE__ */ defineComponent({
598+
__name: \\"union-props\\",
599+
props: {
600+
\\"foo\\": { type: [String, Number], required: true },
601+
\\"bar\\": { type: [String, Number], required: true },
602+
\\"optional\\": { type: Boolean, required: false }
603+
},
604+
setup(__props) {
605+
return () => {
606+
};
607+
}
608+
});
609+
610+
var unionProps = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
611+
612+
export { unionProps as default };
613+
"
614+
`;
615+
616+
exports[`fixtures > tests/fixtures/union-props.vue > isProduction = true 1`] = `
617+
"import { defineComponent } from 'vue';
618+
import _export_sfc from '/plugin-vue/export-helper';
619+
620+
var _sfc_main = /* @__PURE__ */ defineComponent({
621+
__name: \\"union-props\\",
622+
props: {
623+
\\"foo\\": null,
624+
\\"bar\\": null,
625+
\\"optional\\": { type: Boolean }
626+
},
627+
setup(__props) {
628+
return () => {
629+
};
630+
}
631+
});
632+
633+
var unionProps = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
634+
635+
export { unionProps as default };
636+
"
637+
`;
638+
593639
exports[`fixtures > tests/fixtures/unresolved.vue > isProduction = false 1`] = `
594640
"import { defineComponent } from 'vue';
595641
import _export_sfc from '/plugin-vue/export-helper';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script setup lang="ts">
2+
type T = 'foo' | 'bar'
3+
defineProps<
4+
{ [K in T]: string | number } & {
5+
[K in 'optional']?: boolean
6+
}
7+
>()
8+
</script>

0 commit comments

Comments
 (0)