Skip to content

fix(compiler-sfc): fix type inference issue with the keyof operator #10921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 7, 2024
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