Skip to content

Commit 760755f

Browse files
committed
feat(compiler-sfc): support string/number indexed types in macros
1 parent 8d8ddd6 commit 760755f

File tree

2 files changed

+111
-41
lines changed

2 files changed

+111
-41
lines changed

Diff for: packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('resolveType', () => {
232232
})
233233
})
234234

235-
test('indexed access type', () => {
235+
test('indexed access type (literal)', () => {
236236
expect(
237237
resolve(`
238238
type T = { bar: number }
@@ -244,6 +244,37 @@ describe('resolveType', () => {
244244
})
245245
})
246246

247+
test('indexed access type (advanced)', () => {
248+
expect(
249+
resolve(`
250+
type K = 'foo' | 'bar'
251+
type T = { foo: string, bar: number }
252+
type S = { foo: { foo: T[string] }, bar: { bar: string } }
253+
defineProps<S[K]>()
254+
`).props
255+
).toStrictEqual({
256+
foo: ['String', 'Number'],
257+
bar: ['String']
258+
})
259+
})
260+
261+
test('indexed access type (number)', () => {
262+
expect(
263+
resolve(`
264+
type A = (string | number)[]
265+
type AA = Array<string>
266+
type T = [1, 'foo']
267+
type TT = [foo: 1, bar: 'foo']
268+
defineProps<{ foo: A[number], bar: AA[number], tuple: T[number], namedTuple: TT[number] }>()
269+
`).props
270+
).toStrictEqual({
271+
foo: ['String', 'Number'],
272+
bar: ['String'],
273+
tuple: ['Number', 'String'],
274+
namedTuple: ['Number', 'String']
275+
})
276+
})
277+
247278
test('namespace', () => {
248279
expect(
249280
resolve(`
@@ -396,7 +427,7 @@ describe('resolveType', () => {
396427

397428
test('unsupported index type', () => {
398429
expect(() => resolve(`defineProps<X[K]>()`)).toThrow(
399-
`Unsupported index type`
430+
`Unsupported type when resolving index type`
400431
)
401432
})
402433

Diff for: packages/compiler-sfc/src/script/resolveType.ts

+78-39
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TSEnumDeclaration,
88
TSExpressionWithTypeArguments,
99
TSFunctionType,
10+
TSIndexedAccessType,
1011
TSInterfaceDeclaration,
1112
TSMappedType,
1213
TSMethodSignature,
@@ -117,30 +118,11 @@ function innerResolveTypeElements(
117118
case 'TSMappedType':
118119
return resolveMappedType(ctx, node, scope)
119120
case 'TSIndexedAccessType': {
120-
if (
121-
node.indexType.type === 'TSLiteralType' &&
122-
node.indexType.literal.type === 'StringLiteral'
123-
) {
124-
const resolved = resolveTypeElements(ctx, node.objectType, scope)
125-
const key = node.indexType.literal.value
126-
const targetType = resolved.props[key].typeAnnotation
127-
if (targetType) {
128-
return resolveTypeElements(
129-
ctx,
130-
targetType.typeAnnotation,
131-
resolved.props[key]._ownerScope
132-
)
133-
} else {
134-
break
135-
}
136-
} else {
137-
// TODO support `number` and `string` index type when possible
138-
ctx.error(
139-
`Unsupported index type: ${node.indexType.type}`,
140-
node.indexType,
141-
scope
142-
)
143-
}
121+
const types = resolveIndexType(ctx, node, scope)
122+
return mergeElements(
123+
types.map(t => resolveTypeElements(ctx, t, t._ownerScope)),
124+
'TSUnionType'
125+
)
144126
}
145127
case 'TSExpressionWithTypeArguments': // referenced by interface extends
146128
case 'TSTypeReference': {
@@ -201,6 +183,7 @@ function mergeElements(
201183
maps: ResolvedElements[],
202184
type: 'TSUnionType' | 'TSIntersectionType'
203185
): ResolvedElements {
186+
if (maps.length === 1) return maps[0]
204187
const res: ResolvedElements = { props: {} }
205188
const { props: baseProps } = res
206189
for (const { props, calls } of maps) {
@@ -282,6 +265,66 @@ function resolveMappedType(
282265
return res
283266
}
284267

268+
function resolveIndexType(
269+
ctx: ScriptCompileContext,
270+
node: TSIndexedAccessType,
271+
scope: TypeScope
272+
): (TSType & WithScope)[] {
273+
if (node.indexType.type === 'TSNumberKeyword') {
274+
return resolveArrayElementType(ctx, node.objectType, scope)
275+
}
276+
277+
const { indexType, objectType } = node
278+
const types: TSType[] = []
279+
let keys: string[]
280+
let resolved: ResolvedElements
281+
if (indexType.type === 'TSStringKeyword') {
282+
resolved = resolveTypeElements(ctx, objectType, scope)
283+
keys = Object.keys(resolved.props)
284+
} else {
285+
keys = resolveStringType(ctx, indexType, scope)
286+
resolved = resolveTypeElements(ctx, objectType, scope)
287+
}
288+
for (const key of keys) {
289+
const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation
290+
if (targetType) {
291+
;(targetType as TSType & WithScope)._ownerScope =
292+
resolved.props[key]._ownerScope
293+
types.push(targetType)
294+
}
295+
}
296+
return types
297+
}
298+
299+
function resolveArrayElementType(
300+
ctx: ScriptCompileContext,
301+
node: Node,
302+
scope: TypeScope
303+
): TSType[] {
304+
// type[]
305+
if (node.type === 'TSArrayType') {
306+
return [node.elementType]
307+
}
308+
// tuple
309+
if (node.type === 'TSTupleType') {
310+
return node.elementTypes.map(t =>
311+
t.type === 'TSNamedTupleMember' ? t.elementType : t
312+
)
313+
}
314+
if (node.type === 'TSTypeReference') {
315+
// Array<type>
316+
if (getReferenceName(node) === 'Array' && node.typeParameters) {
317+
return node.typeParameters.params
318+
} else {
319+
const resolved = resolveTypeReference(ctx, node, scope)
320+
if (resolved) {
321+
return resolveArrayElementType(ctx, resolved, scope)
322+
}
323+
}
324+
}
325+
ctx.error('Failed to resolve element type from target type', node)
326+
}
327+
285328
function resolveStringType(
286329
ctx: ScriptCompileContext,
287330
node: Node,
@@ -322,15 +365,15 @@ function resolveStringType(
322365
return getParam().map(s => s[0].toLowerCase() + s.slice(1))
323366
default:
324367
ctx.error(
325-
'Unsupported type when resolving string type',
368+
'Unsupported type when resolving index type',
326369
node.typeName,
327370
scope
328371
)
329372
}
330373
}
331374
}
332375
}
333-
ctx.error('Failed to resolve string type into finite keys', node, scope)
376+
ctx.error('Failed to resolve index type into finite keys', node, scope)
334377
}
335378

336379
function resolveTemplateKeys(
@@ -991,19 +1034,12 @@ export function inferRuntimeType(
9911034
return ['Symbol']
9921035

9931036
case 'TSIndexedAccessType': {
994-
if (
995-
node.indexType.type === 'TSLiteralType' &&
996-
node.indexType.literal.type === 'StringLiteral'
997-
) {
998-
try {
999-
const resolved = resolveTypeElements(ctx, node.objectType, scope)
1000-
const key = node.indexType.literal.value
1001-
const prop = resolved.props[key]
1002-
return inferRuntimeType(ctx, prop, prop._ownerScope)
1003-
} catch (e) {
1004-
// avoid hard error, fallback to unknown
1005-
return [UNKNOWN_TYPE]
1006-
}
1037+
try {
1038+
const types = resolveIndexType(ctx, node, scope)
1039+
return flattenTypes(ctx, types, scope)
1040+
} catch (e) {
1041+
// avoid hard error, fallback to unknown
1042+
return [UNKNOWN_TYPE]
10071043
}
10081044
}
10091045

@@ -1020,6 +1056,9 @@ function flattenTypes(
10201056
types: TSType[],
10211057
scope: TypeScope
10221058
): string[] {
1059+
if (types.length === 1) {
1060+
return inferRuntimeType(ctx, types[0], scope)
1061+
}
10231062
return [
10241063
...new Set(
10251064
([] as string[]).concat(

0 commit comments

Comments
 (0)