From 293cf4e131b6d4606e1de2cd7ea87814e2544952 Mon Sep 17 00:00:00 2001 From: Tycho Date: Fri, 7 Jun 2024 16:27:43 +0800 Subject: [PATCH] fix(compiler-sfc): improve type resolving for the keyof operator (#10921) close #10920 close #11002 --- .../compileScript/resolveType.spec.ts | 84 +++++++- .../compiler-sfc/src/script/resolveType.ts | 204 ++++++++++++------ 2 files changed, 213 insertions(+), 75 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 98f5019a03d..697227794b0 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -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 declare const arr: Array - 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 + type Bar = { + [key: string]: 1 + [key: Test]: 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 + type Bar = { [key: string]: any } + type AnyRecord = Record + type Baz = { a: 1, ${1}: 2, b: 3} + + defineProps<{ + record: keyof Foo, + anyRecord: keyof AnyRecord + partial: keyof Partial, + required: keyof Required, + readonly: keyof Readonly, + pick: keyof Pick + extract: keyof Extract + }>() + `, + ) + + 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( ` diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index bbed11baffe..f5cac0fbb29 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -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