Skip to content

Commit fb8ecc8

Browse files
committed
feat(compiler-sfc): support mapped types, string types & template type in macros
1 parent d1f973b commit fb8ecc8

File tree

2 files changed

+159
-28
lines changed

2 files changed

+159
-28
lines changed

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

+41-2
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,48 @@ describe('resolveType', () => {
147147
})
148148
})
149149

150-
// describe('built-in utility types', () => {
150+
test('template string type', () => {
151+
expect(
152+
resolve(`
153+
type T = 'foo' | 'bar'
154+
type S = 'x' | 'y'
155+
type Target = {
156+
[\`_\${T}_\${S}_\`]: string
157+
}
158+
`).elements
159+
).toStrictEqual({
160+
_foo_x_: ['String'],
161+
_foo_y_: ['String'],
162+
_bar_x_: ['String'],
163+
_bar_y_: ['String']
164+
})
165+
})
151166

152-
// })
167+
test('mapped types w/ string manipulation', () => {
168+
expect(
169+
resolve(`
170+
type T = 'foo' | 'bar'
171+
type Target = { [K in T]: string | number } & {
172+
[K in 'optional']?: boolean
173+
} & {
174+
[K in Capitalize<T>]: string
175+
} & {
176+
[K in Uppercase<Extract<T, 'foo'>>]: string
177+
} & {
178+
[K in \`x\${T}\`]: string
179+
}
180+
`).elements
181+
).toStrictEqual({
182+
foo: ['String', 'Number'],
183+
bar: ['String', 'Number'],
184+
Foo: ['String'],
185+
Bar: ['String'],
186+
FOO: ['String'],
187+
xfoo: ['String'],
188+
xbar: ['String'],
189+
optional: ['Boolean']
190+
})
191+
})
153192

154193
describe('errors', () => {
155194
test('error on computed keys', () => {

packages/compiler-sfc/src/script/resolveType.ts

+118-26
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ import {
55
TSEnumDeclaration,
66
TSExpressionWithTypeArguments,
77
TSFunctionType,
8+
TSMappedType,
89
TSMethodSignature,
910
TSPropertySignature,
1011
TSType,
1112
TSTypeAnnotation,
1213
TSTypeElement,
13-
TSTypeReference
14+
TSTypeReference,
15+
TemplateLiteral
1416
} from '@babel/types'
1517
import { UNKNOWN_TYPE } from './utils'
1618
import { ScriptCompileContext } from './context'
1719
import { ImportBinding } from '../compileScript'
1820
import { TSInterfaceDeclaration } from '@babel/types'
19-
import { hasOwn, isArray } from '@vue/shared'
21+
import { capitalize, hasOwn, isArray } from '@vue/shared'
2022
import { Expression } from '@babel/types'
2123

2224
export interface TypeScope {
@@ -65,14 +67,23 @@ function innerResolveTypeElements(
6567
return ret
6668
}
6769
case 'TSExpressionWithTypeArguments': // referenced by interface extends
68-
case 'TSTypeReference':
69-
return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
70+
case 'TSTypeReference': {
71+
const resolved = resolveTypeReference(ctx, node)
72+
if (resolved) {
73+
return resolveTypeElements(ctx, resolved)
74+
} else {
75+
// TODO Pick / Omit
76+
ctx.error(`Failed to resolved type reference`, node)
77+
}
78+
}
7079
case 'TSUnionType':
7180
case 'TSIntersectionType':
7281
return mergeElements(
7382
node.types.map(t => resolveTypeElements(ctx, t)),
7483
node.type
7584
)
85+
case 'TSMappedType':
86+
return resolveMappedType(ctx, node)
7687
}
7788
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
7889
}
@@ -113,6 +124,10 @@ function typeElementsToMap(
113124
: null
114125
if (name && !e.computed) {
115126
ret[name] = e
127+
} else if (e.key.type === 'TemplateLiteral') {
128+
for (const key of resolveTemplateKeys(ctx, e.key)) {
129+
ret[key] = e
130+
}
116131
} else {
117132
ctx.error(
118133
`computed keys are not supported in types referenced by SFC macros.`,
@@ -136,7 +151,11 @@ function mergeElements(
136151
if (!(key in res)) {
137152
res[key] = m[key]
138153
} else {
139-
res[key] = createProperty(res[key].key, type, [res[key], m[key]])
154+
res[key] = createProperty(res[key].key, {
155+
type,
156+
// @ts-ignore
157+
types: [res[key], m[key]]
158+
})
140159
}
141160
}
142161
if (m.__callSignatures) {
@@ -148,19 +167,15 @@ function mergeElements(
148167

149168
function createProperty(
150169
key: Expression,
151-
type: 'TSUnionType' | 'TSIntersectionType',
152-
types: Node[]
170+
typeAnnotation: TSType
153171
): TSPropertySignature {
154172
return {
155173
type: 'TSPropertySignature',
156174
key,
157175
kind: 'get',
158176
typeAnnotation: {
159177
type: 'TSTypeAnnotation',
160-
typeAnnotation: {
161-
type,
162-
types: types as TSType[]
163-
}
178+
typeAnnotation
164179
}
165180
}
166181
}
@@ -183,22 +198,102 @@ function resolveInterfaceMembers(
183198
return base
184199
}
185200

186-
function resolveTypeReference(
201+
function resolveMappedType(
187202
ctx: ScriptCompileContext,
188-
node: TSTypeReference | TSExpressionWithTypeArguments,
189-
scope?: TypeScope
190-
): Node
191-
function resolveTypeReference(
203+
node: TSMappedType
204+
): ResolvedElements {
205+
const res: ResolvedElements = {}
206+
if (!node.typeParameter.constraint) {
207+
ctx.error(`mapped type used in macros must have a finite constraint.`, node)
208+
}
209+
const keys = resolveStringType(ctx, node.typeParameter.constraint)
210+
for (const key of keys) {
211+
res[key] = createProperty(
212+
{
213+
type: 'Identifier',
214+
name: key
215+
},
216+
node.typeAnnotation!
217+
)
218+
}
219+
return res
220+
}
221+
222+
function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] {
223+
switch (node.type) {
224+
case 'StringLiteral':
225+
return [node.value]
226+
case 'TSLiteralType':
227+
return resolveStringType(ctx, node.literal)
228+
case 'TSUnionType':
229+
return node.types.map(t => resolveStringType(ctx, t)).flat()
230+
case 'TemplateLiteral': {
231+
return resolveTemplateKeys(ctx, node)
232+
}
233+
case 'TSTypeReference': {
234+
const resolved = resolveTypeReference(ctx, node)
235+
if (resolved) {
236+
return resolveStringType(ctx, resolved)
237+
}
238+
if (node.typeName.type === 'Identifier') {
239+
const getParam = (index = 0) =>
240+
resolveStringType(ctx, node.typeParameters!.params[index])
241+
switch (node.typeName.name) {
242+
case 'Extract':
243+
return getParam(1)
244+
case 'Exclude': {
245+
const excluded = getParam(1)
246+
return getParam().filter(s => !excluded.includes(s))
247+
}
248+
case 'Uppercase':
249+
return getParam().map(s => s.toUpperCase())
250+
case 'Lowercase':
251+
return getParam().map(s => s.toLowerCase())
252+
case 'Capitalize':
253+
return getParam().map(capitalize)
254+
case 'Uncapitalize':
255+
return getParam().map(s => s[0].toLowerCase() + s.slice(1))
256+
default:
257+
ctx.error('Failed to resolve type reference', node)
258+
}
259+
}
260+
}
261+
}
262+
ctx.error('Failed to resolve string type into finite keys', node)
263+
}
264+
265+
function resolveTemplateKeys(
192266
ctx: ScriptCompileContext,
193-
node: TSTypeReference | TSExpressionWithTypeArguments,
194-
scope: TypeScope,
195-
bail: false
196-
): Node | undefined
267+
node: TemplateLiteral
268+
): string[] {
269+
if (!node.expressions.length) {
270+
return [node.quasis[0].value.raw]
271+
}
272+
273+
const res: string[] = []
274+
const e = node.expressions[0]
275+
const q = node.quasis[0]
276+
const leading = q ? q.value.raw : ``
277+
const resolved = resolveStringType(ctx, e)
278+
const restResolved = resolveTemplateKeys(ctx, {
279+
...node,
280+
expressions: node.expressions.slice(1),
281+
quasis: q ? node.quasis.slice(1) : node.quasis
282+
})
283+
284+
for (const r of resolved) {
285+
for (const rr of restResolved) {
286+
res.push(leading + r + rr)
287+
}
288+
}
289+
290+
return res
291+
}
292+
197293
function resolveTypeReference(
198294
ctx: ScriptCompileContext,
199295
node: TSTypeReference | TSExpressionWithTypeArguments,
200-
scope = getRootScope(ctx),
201-
bail = true
296+
scope = getRootScope(ctx)
202297
): Node | undefined {
203298
const ref = node.type === 'TSTypeReference' ? node.typeName : node.expression
204299
if (ref.type === 'Identifier') {
@@ -211,9 +306,6 @@ function resolveTypeReference(
211306
// TODO qualified name, e.g. Foo.Bar
212307
// return resolveTypeReference()
213308
}
214-
if (bail) {
215-
ctx.error('Failed to resolve type reference.', node)
216-
}
217309
}
218310

219311
function getRootScope(ctx: ScriptCompileContext): TypeScope {
@@ -332,7 +424,7 @@ export function inferRuntimeType(
332424

333425
case 'TSTypeReference':
334426
if (node.typeName.type === 'Identifier') {
335-
const resolved = resolveTypeReference(ctx, node, scope, false)
427+
const resolved = resolveTypeReference(ctx, node, scope)
336428
if (resolved) {
337429
return inferRuntimeType(ctx, resolved, scope)
338430
}

0 commit comments

Comments
 (0)