feat(compiler-sfc): support string/number indexed types in macros
This commit is contained in:
parent
8d8ddd686c
commit
760755f4f8
|
@ -232,7 +232,7 @@ describe('resolveType', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('indexed access type', () => {
|
test('indexed access type (literal)', () => {
|
||||||
expect(
|
expect(
|
||||||
resolve(`
|
resolve(`
|
||||||
type T = { bar: number }
|
type T = { bar: number }
|
||||||
|
@ -244,6 +244,37 @@ describe('resolveType', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('indexed access type (advanced)', () => {
|
||||||
|
expect(
|
||||||
|
resolve(`
|
||||||
|
type K = 'foo' | 'bar'
|
||||||
|
type T = { foo: string, bar: number }
|
||||||
|
type S = { foo: { foo: T[string] }, bar: { bar: string } }
|
||||||
|
defineProps<S[K]>()
|
||||||
|
`).props
|
||||||
|
).toStrictEqual({
|
||||||
|
foo: ['String', 'Number'],
|
||||||
|
bar: ['String']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('indexed access type (number)', () => {
|
||||||
|
expect(
|
||||||
|
resolve(`
|
||||||
|
type A = (string | number)[]
|
||||||
|
type AA = Array<string>
|
||||||
|
type T = [1, 'foo']
|
||||||
|
type TT = [foo: 1, bar: 'foo']
|
||||||
|
defineProps<{ foo: A[number], bar: AA[number], tuple: T[number], namedTuple: TT[number] }>()
|
||||||
|
`).props
|
||||||
|
).toStrictEqual({
|
||||||
|
foo: ['String', 'Number'],
|
||||||
|
bar: ['String'],
|
||||||
|
tuple: ['Number', 'String'],
|
||||||
|
namedTuple: ['Number', 'String']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('namespace', () => {
|
test('namespace', () => {
|
||||||
expect(
|
expect(
|
||||||
resolve(`
|
resolve(`
|
||||||
|
@ -396,7 +427,7 @@ describe('resolveType', () => {
|
||||||
|
|
||||||
test('unsupported index type', () => {
|
test('unsupported index type', () => {
|
||||||
expect(() => resolve(`defineProps<X[K]>()`)).toThrow(
|
expect(() => resolve(`defineProps<X[K]>()`)).toThrow(
|
||||||
`Unsupported index type`
|
`Unsupported type when resolving index type`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
TSEnumDeclaration,
|
TSEnumDeclaration,
|
||||||
TSExpressionWithTypeArguments,
|
TSExpressionWithTypeArguments,
|
||||||
TSFunctionType,
|
TSFunctionType,
|
||||||
|
TSIndexedAccessType,
|
||||||
TSInterfaceDeclaration,
|
TSInterfaceDeclaration,
|
||||||
TSMappedType,
|
TSMappedType,
|
||||||
TSMethodSignature,
|
TSMethodSignature,
|
||||||
|
@ -117,30 +118,11 @@ function innerResolveTypeElements(
|
||||||
case 'TSMappedType':
|
case 'TSMappedType':
|
||||||
return resolveMappedType(ctx, node, scope)
|
return resolveMappedType(ctx, node, scope)
|
||||||
case 'TSIndexedAccessType': {
|
case 'TSIndexedAccessType': {
|
||||||
if (
|
const types = resolveIndexType(ctx, node, scope)
|
||||||
node.indexType.type === 'TSLiteralType' &&
|
return mergeElements(
|
||||||
node.indexType.literal.type === 'StringLiteral'
|
types.map(t => resolveTypeElements(ctx, t, t._ownerScope)),
|
||||||
) {
|
'TSUnionType'
|
||||||
const resolved = resolveTypeElements(ctx, node.objectType, scope)
|
|
||||||
const key = node.indexType.literal.value
|
|
||||||
const targetType = resolved.props[key].typeAnnotation
|
|
||||||
if (targetType) {
|
|
||||||
return resolveTypeElements(
|
|
||||||
ctx,
|
|
||||||
targetType.typeAnnotation,
|
|
||||||
resolved.props[key]._ownerScope
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO support `number` and `string` index type when possible
|
|
||||||
ctx.error(
|
|
||||||
`Unsupported index type: ${node.indexType.type}`,
|
|
||||||
node.indexType,
|
|
||||||
scope
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case 'TSExpressionWithTypeArguments': // referenced by interface extends
|
case 'TSExpressionWithTypeArguments': // referenced by interface extends
|
||||||
case 'TSTypeReference': {
|
case 'TSTypeReference': {
|
||||||
|
@ -201,6 +183,7 @@ function mergeElements(
|
||||||
maps: ResolvedElements[],
|
maps: ResolvedElements[],
|
||||||
type: 'TSUnionType' | 'TSIntersectionType'
|
type: 'TSUnionType' | 'TSIntersectionType'
|
||||||
): ResolvedElements {
|
): ResolvedElements {
|
||||||
|
if (maps.length === 1) return maps[0]
|
||||||
const res: ResolvedElements = { props: {} }
|
const res: ResolvedElements = { props: {} }
|
||||||
const { props: baseProps } = res
|
const { props: baseProps } = res
|
||||||
for (const { props, calls } of maps) {
|
for (const { props, calls } of maps) {
|
||||||
|
@ -282,6 +265,66 @@ function resolveMappedType(
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveIndexType(
|
||||||
|
ctx: ScriptCompileContext,
|
||||||
|
node: TSIndexedAccessType,
|
||||||
|
scope: TypeScope
|
||||||
|
): (TSType & WithScope)[] {
|
||||||
|
if (node.indexType.type === 'TSNumberKeyword') {
|
||||||
|
return resolveArrayElementType(ctx, node.objectType, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { indexType, objectType } = node
|
||||||
|
const types: TSType[] = []
|
||||||
|
let keys: string[]
|
||||||
|
let resolved: ResolvedElements
|
||||||
|
if (indexType.type === 'TSStringKeyword') {
|
||||||
|
resolved = resolveTypeElements(ctx, objectType, scope)
|
||||||
|
keys = Object.keys(resolved.props)
|
||||||
|
} else {
|
||||||
|
keys = resolveStringType(ctx, indexType, scope)
|
||||||
|
resolved = resolveTypeElements(ctx, objectType, scope)
|
||||||
|
}
|
||||||
|
for (const key of keys) {
|
||||||
|
const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation
|
||||||
|
if (targetType) {
|
||||||
|
;(targetType as TSType & WithScope)._ownerScope =
|
||||||
|
resolved.props[key]._ownerScope
|
||||||
|
types.push(targetType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveArrayElementType(
|
||||||
|
ctx: ScriptCompileContext,
|
||||||
|
node: Node,
|
||||||
|
scope: TypeScope
|
||||||
|
): TSType[] {
|
||||||
|
// type[]
|
||||||
|
if (node.type === 'TSArrayType') {
|
||||||
|
return [node.elementType]
|
||||||
|
}
|
||||||
|
// tuple
|
||||||
|
if (node.type === 'TSTupleType') {
|
||||||
|
return node.elementTypes.map(t =>
|
||||||
|
t.type === 'TSNamedTupleMember' ? t.elementType : t
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (node.type === 'TSTypeReference') {
|
||||||
|
// Array<type>
|
||||||
|
if (getReferenceName(node) === 'Array' && node.typeParameters) {
|
||||||
|
return node.typeParameters.params
|
||||||
|
} else {
|
||||||
|
const resolved = resolveTypeReference(ctx, node, scope)
|
||||||
|
if (resolved) {
|
||||||
|
return resolveArrayElementType(ctx, resolved, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.error('Failed to resolve element type from target type', node)
|
||||||
|
}
|
||||||
|
|
||||||
function resolveStringType(
|
function resolveStringType(
|
||||||
ctx: ScriptCompileContext,
|
ctx: ScriptCompileContext,
|
||||||
node: Node,
|
node: Node,
|
||||||
|
@ -322,7 +365,7 @@ function resolveStringType(
|
||||||
return getParam().map(s => s[0].toLowerCase() + s.slice(1))
|
return getParam().map(s => s[0].toLowerCase() + s.slice(1))
|
||||||
default:
|
default:
|
||||||
ctx.error(
|
ctx.error(
|
||||||
'Unsupported type when resolving string type',
|
'Unsupported type when resolving index type',
|
||||||
node.typeName,
|
node.typeName,
|
||||||
scope
|
scope
|
||||||
)
|
)
|
||||||
|
@ -330,7 +373,7 @@ function resolveStringType(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.error('Failed to resolve string type into finite keys', node, scope)
|
ctx.error('Failed to resolve index type into finite keys', node, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTemplateKeys(
|
function resolveTemplateKeys(
|
||||||
|
@ -991,21 +1034,14 @@ export function inferRuntimeType(
|
||||||
return ['Symbol']
|
return ['Symbol']
|
||||||
|
|
||||||
case 'TSIndexedAccessType': {
|
case 'TSIndexedAccessType': {
|
||||||
if (
|
|
||||||
node.indexType.type === 'TSLiteralType' &&
|
|
||||||
node.indexType.literal.type === 'StringLiteral'
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const resolved = resolveTypeElements(ctx, node.objectType, scope)
|
const types = resolveIndexType(ctx, node, scope)
|
||||||
const key = node.indexType.literal.value
|
return flattenTypes(ctx, types, scope)
|
||||||
const prop = resolved.props[key]
|
|
||||||
return inferRuntimeType(ctx, prop, prop._ownerScope)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// avoid hard error, fallback to unknown
|
// avoid hard error, fallback to unknown
|
||||||
return [UNKNOWN_TYPE]
|
return [UNKNOWN_TYPE]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case 'ClassDeclaration':
|
case 'ClassDeclaration':
|
||||||
return ['Object']
|
return ['Object']
|
||||||
|
@ -1020,6 +1056,9 @@ function flattenTypes(
|
||||||
types: TSType[],
|
types: TSType[],
|
||||||
scope: TypeScope
|
scope: TypeScope
|
||||||
): string[] {
|
): string[] {
|
||||||
|
if (types.length === 1) {
|
||||||
|
return inferRuntimeType(ctx, types[0], scope)
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
...new Set(
|
...new Set(
|
||||||
([] as string[]).concat(
|
([] as string[]).concat(
|
||||||
|
|
Loading…
Reference in New Issue