]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-sfc): support string/number indexed types in macros
authorEvan You <yyx990803@gmail.com>
Sat, 15 Apr 2023 13:46:44 +0000 (21:46 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 15 Apr 2023 14:08:39 +0000 (22:08 +0800)
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
packages/compiler-sfc/src/script/resolveType.ts

index a6aad1ea197109d9d32f31e83888e04f53730db5..7aa08d595f5eaceb938e394c9dc75c1129eed71e 100644 (file)
@@ -232,7 +232,7 @@ describe('resolveType', () => {
     })
   })
 
-  test('indexed access type', () => {
+  test('indexed access type (literal)', () => {
     expect(
       resolve(`
     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', () => {
     expect(
       resolve(`
@@ -396,7 +427,7 @@ describe('resolveType', () => {
 
     test('unsupported index type', () => {
       expect(() => resolve(`defineProps<X[K]>()`)).toThrow(
-        `Unsupported index type`
+        `Unsupported type when resolving index type`
       )
     })
 
index d5661c871aa09be0382dc8ad397351ef3fdaaf70..f1fce65a28739f92f8e993774fbcba7b1812df89 100644 (file)
@@ -7,6 +7,7 @@ import {
   TSEnumDeclaration,
   TSExpressionWithTypeArguments,
   TSFunctionType,
+  TSIndexedAccessType,
   TSInterfaceDeclaration,
   TSMappedType,
   TSMethodSignature,
@@ -117,30 +118,11 @@ function innerResolveTypeElements(
     case 'TSMappedType':
       return resolveMappedType(ctx, node, scope)
     case 'TSIndexedAccessType': {
-      if (
-        node.indexType.type === 'TSLiteralType' &&
-        node.indexType.literal.type === 'StringLiteral'
-      ) {
-        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
-        )
-      }
+      const types = resolveIndexType(ctx, node, scope)
+      return mergeElements(
+        types.map(t => resolveTypeElements(ctx, t, t._ownerScope)),
+        'TSUnionType'
+      )
     }
     case 'TSExpressionWithTypeArguments': // referenced by interface extends
     case 'TSTypeReference': {
@@ -201,6 +183,7 @@ function mergeElements(
   maps: ResolvedElements[],
   type: 'TSUnionType' | 'TSIntersectionType'
 ): ResolvedElements {
+  if (maps.length === 1) return maps[0]
   const res: ResolvedElements = { props: {} }
   const { props: baseProps } = res
   for (const { props, calls } of maps) {
@@ -282,6 +265,66 @@ function resolveMappedType(
   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(
   ctx: ScriptCompileContext,
   node: Node,
@@ -322,7 +365,7 @@ function resolveStringType(
             return getParam().map(s => s[0].toLowerCase() + s.slice(1))
           default:
             ctx.error(
-              'Unsupported type when resolving string type',
+              'Unsupported type when resolving index type',
               node.typeName,
               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(
@@ -991,19 +1034,12 @@ export function inferRuntimeType(
       return ['Symbol']
 
     case 'TSIndexedAccessType': {
-      if (
-        node.indexType.type === 'TSLiteralType' &&
-        node.indexType.literal.type === 'StringLiteral'
-      ) {
-        try {
-          const resolved = resolveTypeElements(ctx, node.objectType, scope)
-          const key = node.indexType.literal.value
-          const prop = resolved.props[key]
-          return inferRuntimeType(ctx, prop, prop._ownerScope)
-        } catch (e) {
-          // avoid hard error, fallback to unknown
-          return [UNKNOWN_TYPE]
-        }
+      try {
+        const types = resolveIndexType(ctx, node, scope)
+        return flattenTypes(ctx, types, scope)
+      } catch (e) {
+        // avoid hard error, fallback to unknown
+        return [UNKNOWN_TYPE]
       }
     }
 
@@ -1020,6 +1056,9 @@ function flattenTypes(
   types: TSType[],
   scope: TypeScope
 ): string[] {
+  if (types.length === 1) {
+    return inferRuntimeType(ctx, types[0], scope)
+  }
   return [
     ...new Set(
       ([] as string[]).concat(