fix(compiler-sfc): improve type resolving for the keyof operator (#10921)

close #10920 
close #11002
This commit is contained in:
Tycho 2024-06-07 16:27:43 +08:00 committed by GitHub
parent 5afc76c229
commit 293cf4e131
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 209 additions and 71 deletions

View File

@ -9,8 +9,9 @@ import {
registerTS, registerTS,
resolveTypeElements, resolveTypeElements,
} from '../../src/script/resolveType' } from '../../src/script/resolveType'
import { UNKNOWN_TYPE } from '../../src/script/utils'
import ts from 'typescript' import ts from 'typescript'
registerTS(() => ts) registerTS(() => ts)
describe('resolveType', () => { describe('resolveType', () => {
@ -128,7 +129,7 @@ describe('resolveType', () => {
defineProps<{ self: any } & Foo & Bar & Baz>() defineProps<{ self: any } & Foo & Bar & Baz>()
`).props, `).props,
).toStrictEqual({ ).toStrictEqual({
self: ['Unknown'], self: [UNKNOWN_TYPE],
foo: ['Number'], foo: ['Number'],
// both Bar & Baz has 'bar', but Baz['bar] is wider so it should be // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
// preferred // preferred
@ -455,13 +456,13 @@ describe('resolveType', () => {
const { props } = resolve( const { props } = resolve(
` `
import { IMP } from './foo' import { IMP } from './foo'
interface Foo { foo: 1, ${1}: 1 } interface Foo { foo: 1, ${1}: 1 }
type Bar = { bar: 1 } type Bar = { bar: 1 }
declare const obj: Bar declare const obj: Bar
declare const set: Set<any> declare const set: Set<any>
declare const arr: Array<any> declare const arr: Array<any>
defineProps<{ defineProps<{
imp: keyof IMP, imp: keyof IMP,
foo: keyof Foo, foo: keyof Foo,
bar: keyof Bar, 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)', () => { test('ExtractPropTypes (element-plus)', () => {
const { props, raw } = resolve( const { props, raw } = resolve(
` `

View File

@ -1476,6 +1476,17 @@ export function inferRuntimeType(
m.key.type === 'NumericLiteral' m.key.type === 'NumericLiteral'
) { ) {
types.add('Number') 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 { } else {
types.add('String') 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': case 'TSPropertySignature':
if (node.typeAnnotation) { if (node.typeAnnotation) {
@ -1533,81 +1546,123 @@ export function inferRuntimeType(
case 'String': case 'String':
case 'Array': case 'Array':
case 'ArrayLike': case 'ArrayLike':
case 'Parameters':
case 'ConstructorParameters':
case 'ReadonlyArray': case 'ReadonlyArray':
return ['String', 'Number'] 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'] 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]
switch (node.typeName.name) { // TS built-in utility types
case 'Array': // https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Function': case 'Partial':
case 'Object': case 'Required':
case 'Set': case 'Readonly':
case 'Map': case 'Record':
case 'WeakSet': case 'Pick':
case 'WeakMap': case 'Omit':
case 'Date': case 'InstanceType':
case 'Promise': return ['Object']
case 'Error':
return [node.typeName.name]
// TS built-in utility types case 'Uppercase':
// https://www.typescriptlang.org/docs/handbook/utility-types.html case 'Lowercase':
case 'Partial': case 'Capitalize':
case 'Required': case 'Uncapitalize':
case 'Readonly': return ['String']
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']
case 'Uppercase': case 'Parameters':
case 'Lowercase': case 'ConstructorParameters':
case 'Capitalize': case 'ReadonlyArray':
case 'Uncapitalize': return ['Array']
return ['String']
case 'Parameters': case 'ReadonlyMap':
case 'ConstructorParameters': return ['Map']
case 'ReadonlyArray': case 'ReadonlySet':
return ['Array'] return ['Set']
case 'ReadonlyMap': case 'NonNullable':
return ['Map'] if (node.typeParameters && node.typeParameters.params[0]) {
case 'ReadonlySet': return inferRuntimeType(
return ['Set'] ctx,
node.typeParameters.params[0],
case 'NonNullable': scope,
if (node.typeParameters && node.typeParameters.params[0]) { ).filter(t => t !== 'null')
return inferRuntimeType( }
ctx, break
node.typeParameters.params[0], case 'Extract':
scope, if (node.typeParameters && node.typeParameters.params[1]) {
).filter(t => t !== 'null') return inferRuntimeType(
} ctx,
break node.typeParameters.params[1],
case 'Extract': scope,
if (node.typeParameters && node.typeParameters.params[1]) { )
return inferRuntimeType( }
ctx, break
node.typeParameters.params[1], case 'Exclude':
scope, case 'OmitThisParameter':
) if (node.typeParameters && node.typeParameters.params[0]) {
} return inferRuntimeType(
break ctx,
case 'Exclude': node.typeParameters.params[0],
case 'OmitThisParameter': scope,
if (node.typeParameters && node.typeParameters.params[0]) { )
return inferRuntimeType( }
ctx, break
node.typeParameters.params[0], }
scope,
)
}
break
} }
} }
// cannot infer, fallback to UNKNOWN: ThisParameterType // cannot infer, fallback to UNKNOWN: ThisParameterType
@ -1674,6 +1729,13 @@ export function inferRuntimeType(
node.operator === 'keyof', node.operator === 'keyof',
) )
} }
case 'TSAnyKeyword': {
if (isKeyOf) {
return ['String', 'Number', 'Symbol']
}
break
}
} }
} catch (e) { } catch (e) {
// always soft fail on failed runtime type inference // always soft fail on failed runtime type inference