]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): improve type resolving for the keyof operator (#10921)
authorTycho <jh.leong@outlook.com>
Fri, 7 Jun 2024 08:27:43 +0000 (16:27 +0800)
committerGitHub <noreply@github.com>
Fri, 7 Jun 2024 08:27:43 +0000 (16:27 +0800)
close #10920
close #11002

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

index 98f5019a03da21a541e28d248695f88d13af32d9..697227794b007f74757b46da3541eb9e8151d549 100644 (file)
@@ -9,8 +9,9 @@ import {
   registerTS,
   resolveTypeElements,
 } from '../../src/script/resolveType'
-
+import { UNKNOWN_TYPE } from '../../src/script/utils'
 import ts from 'typescript'
+
 registerTS(() => ts)
 
 describe('resolveType', () => {
@@ -128,7 +129,7 @@ describe('resolveType', () => {
     defineProps<{ self: any } & Foo & Bar & Baz>()
     `).props,
     ).toStrictEqual({
-      self: ['Unknown'],
+      self: [UNKNOWN_TYPE],
       foo: ['Number'],
       // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
       // preferred
@@ -455,13 +456,13 @@ describe('resolveType', () => {
     const { props } = resolve(
       `
       import { IMP } from './foo'
-      interface Foo { foo: 1, ${1}: 1 } 
+      interface Foo { foo: 1, ${1}: 1 }
       type Bar = { bar: 1 }
       declare const obj: Bar
       declare const set: Set<any>
       declare const arr: Array<any>
 
-      defineProps<{ 
+      defineProps<{
         imp: keyof IMP,
         foo: keyof Foo,
         bar: keyof Bar,
@@ -483,6 +484,81 @@ describe('resolveType', () => {
     })
   })
 
+  test('keyof: index signature', () => {
+    const { props } = resolve(
+      `
+      declare const num: number;
+      interface Foo {
+        [key: symbol]: 1
+        [key: string]: 1
+        [key: typeof num]: 1,
+      }
+
+      type Test<T> = T
+      type Bar = {
+        [key: string]: 1
+        [key: Test<number>]: 1
+      }
+
+      defineProps<{
+        foo: keyof Foo 
+        bar: keyof Bar
+      }>()
+      `,
+    )
+
+    expect(props).toStrictEqual({
+      foo: ['Symbol', 'String', 'Number'],
+      bar: [UNKNOWN_TYPE],
+    })
+  })
+
+  test('keyof: utility type', () => {
+    const { props } = resolve(
+      `
+      type Foo = Record<symbol | string, any>
+      type Bar = { [key: string]: any }
+      type AnyRecord = Record<keyof any, any>
+      type Baz = { a: 1, ${1}: 2, b: 3}
+
+      defineProps<{
+        record: keyof Foo,
+        anyRecord: keyof AnyRecord 
+        partial: keyof Partial<Bar>,
+        required: keyof Required<Bar>,
+        readonly: keyof Readonly<Bar>,
+        pick: keyof Pick<Baz, 'a' | 1>
+        extract: keyof Extract<keyof Baz, 'a' | 1>
+      }>()
+      `,
+    )
+
+    expect(props).toStrictEqual({
+      record: ['Symbol', 'String'],
+      anyRecord: ['String', 'Number', 'Symbol'],
+      partial: ['String'],
+      required: ['String'],
+      readonly: ['String'],
+      pick: ['String', 'Number'],
+      extract: ['String', 'Number'],
+    })
+  })
+
+  test('keyof: fallback to Unknown', () => {
+    const { props } = resolve(
+      `
+      interface Barr {}
+      interface Bar extends Barr {}
+      type Foo = keyof Bar
+      defineProps<{ foo: Foo }>()
+      `,
+    )
+
+    expect(props).toStrictEqual({
+      foo: [UNKNOWN_TYPE],
+    })
+  })
+
   test('ExtractPropTypes (element-plus)', () => {
     const { props, raw } = resolve(
       `
index bbed11baffe75e78b769b76624a539141b9ee978..f5cac0fbb2943e86806c68b479fd6d873b8049b6 100644 (file)
@@ -1476,6 +1476,17 @@ export function inferRuntimeType(
               m.key.type === 'NumericLiteral'
             ) {
               types.add('Number')
+            } else if (m.type === 'TSIndexSignature') {
+              const annotation = m.parameters[0].typeAnnotation
+              if (annotation && annotation.type !== 'Noop') {
+                const type = inferRuntimeType(
+                  ctx,
+                  annotation.typeAnnotation,
+                  scope,
+                )[0]
+                if (type === UNKNOWN_TYPE) return [UNKNOWN_TYPE]
+                types.add(type)
+              }
             } else {
               types.add('String')
             }
@@ -1489,7 +1500,9 @@ export function inferRuntimeType(
           }
         }
 
-        return types.size ? Array.from(types) : ['Object']
+        return types.size
+          ? Array.from(types)
+          : [isKeyOf ? UNKNOWN_TYPE : 'Object']
       }
       case 'TSPropertySignature':
         if (node.typeAnnotation) {
@@ -1533,81 +1546,123 @@ export function inferRuntimeType(
               case 'String':
               case 'Array':
               case 'ArrayLike':
+              case 'Parameters':
+              case 'ConstructorParameters':
               case 'ReadonlyArray':
                 return ['String', 'Number']
-              default:
+
+              // TS built-in utility types
+              case 'Record':
+              case 'Partial':
+              case 'Required':
+              case 'Readonly':
+                if (node.typeParameters && node.typeParameters.params[0]) {
+                  return inferRuntimeType(
+                    ctx,
+                    node.typeParameters.params[0],
+                    scope,
+                    true,
+                  )
+                }
+                break
+              case 'Pick':
+              case 'Extract':
+                if (node.typeParameters && node.typeParameters.params[1]) {
+                  return inferRuntimeType(
+                    ctx,
+                    node.typeParameters.params[1],
+                    scope,
+                  )
+                }
+                break
+
+              case 'Function':
+              case 'Object':
+              case 'Set':
+              case 'Map':
+              case 'WeakSet':
+              case 'WeakMap':
+              case 'Date':
+              case 'Promise':
+              case 'Error':
+              case 'Uppercase':
+              case 'Lowercase':
+              case 'Capitalize':
+              case 'Uncapitalize':
+              case 'ReadonlyMap':
+              case 'ReadonlySet':
                 return ['String']
             }
-          }
+          } else {
+            switch (node.typeName.name) {
+              case 'Array':
+              case 'Function':
+              case 'Object':
+              case 'Set':
+              case 'Map':
+              case 'WeakSet':
+              case 'WeakMap':
+              case 'Date':
+              case 'Promise':
+              case 'Error':
+                return [node.typeName.name]
+
+              // TS built-in utility types
+              // https://www.typescriptlang.org/docs/handbook/utility-types.html
+              case 'Partial':
+              case 'Required':
+              case 'Readonly':
+              case 'Record':
+              case 'Pick':
+              case 'Omit':
+              case 'InstanceType':
+                return ['Object']
+
+              case 'Uppercase':
+              case 'Lowercase':
+              case 'Capitalize':
+              case 'Uncapitalize':
+                return ['String']
 
-          switch (node.typeName.name) {
-            case 'Array':
-            case 'Function':
-            case 'Object':
-            case 'Set':
-            case 'Map':
-            case 'WeakSet':
-            case 'WeakMap':
-            case 'Date':
-            case 'Promise':
-            case 'Error':
-              return [node.typeName.name]
-
-            // TS built-in utility types
-            // https://www.typescriptlang.org/docs/handbook/utility-types.html
-            case 'Partial':
-            case 'Required':
-            case 'Readonly':
-            case 'Record':
-            case 'Pick':
-            case 'Omit':
-            case 'InstanceType':
-              return ['Object']
-
-            case 'Uppercase':
-            case 'Lowercase':
-            case 'Capitalize':
-            case 'Uncapitalize':
-              return ['String']
-
-            case 'Parameters':
-            case 'ConstructorParameters':
-            case 'ReadonlyArray':
-              return ['Array']
-
-            case 'ReadonlyMap':
-              return ['Map']
-            case 'ReadonlySet':
-              return ['Set']
-
-            case 'NonNullable':
-              if (node.typeParameters && node.typeParameters.params[0]) {
-                return inferRuntimeType(
-                  ctx,
-                  node.typeParameters.params[0],
-                  scope,
-                ).filter(t => t !== 'null')
-              }
-              break
-            case 'Extract':
-              if (node.typeParameters && node.typeParameters.params[1]) {
-                return inferRuntimeType(
-                  ctx,
-                  node.typeParameters.params[1],
-                  scope,
-                )
-              }
-              break
-            case 'Exclude':
-            case 'OmitThisParameter':
-              if (node.typeParameters && node.typeParameters.params[0]) {
-                return inferRuntimeType(
-                  ctx,
-                  node.typeParameters.params[0],
-                  scope,
-                )
-              }
-              break
+              case 'Parameters':
+              case 'ConstructorParameters':
+              case 'ReadonlyArray':
+                return ['Array']
+
+              case 'ReadonlyMap':
+                return ['Map']
+              case 'ReadonlySet':
+                return ['Set']
+
+              case 'NonNullable':
+                if (node.typeParameters && node.typeParameters.params[0]) {
+                  return inferRuntimeType(
+                    ctx,
+                    node.typeParameters.params[0],
+                    scope,
+                  ).filter(t => t !== 'null')
+                }
+                break
+              case 'Extract':
+                if (node.typeParameters && node.typeParameters.params[1]) {
+                  return inferRuntimeType(
+                    ctx,
+                    node.typeParameters.params[1],
+                    scope,
+                  )
+                }
+                break
+              case 'Exclude':
+              case 'OmitThisParameter':
+                if (node.typeParameters && node.typeParameters.params[0]) {
+                  return inferRuntimeType(
+                    ctx,
+                    node.typeParameters.params[0],
+                    scope,
+                  )
+                }
+                break
+            }
           }
         }
         // cannot infer, fallback to UNKNOWN: ThisParameterType
@@ -1674,6 +1729,13 @@ export function inferRuntimeType(
           node.operator === 'keyof',
         )
       }
+
+      case 'TSAnyKeyword': {
+        if (isKeyOf) {
+          return ['String', 'Number', 'Symbol']
+        }
+        break
+      }
     }
   } catch (e) {
     // always soft fail on failed runtime type inference