Skip to content

Commit

Permalink
fix(compiler-sfc): improve type resolving for the keyof operator (#10921
Browse files Browse the repository at this point in the history
)

close #10920 
close #11002
  • Loading branch information
jh-leong authored Jun 7, 2024
1 parent 5afc76c commit 293cf4e
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 75 deletions.
84 changes: 80 additions & 4 deletions packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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(
`
Expand Down
204 changes: 133 additions & 71 deletions packages/compiler-sfc/src/script/resolveType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 293cf4e

Please sign in to comment.