Skip to content

Commit 293cf4e

Browse files
authored
fix(compiler-sfc): improve type resolving for the keyof operator (#10921)
close #10920 close #11002
1 parent 5afc76c commit 293cf4e

File tree

2 files changed

+213
-75
lines changed

2 files changed

+213
-75
lines changed

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

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
registerTS,
1010
resolveTypeElements,
1111
} from '../../src/script/resolveType'
12-
12+
import { UNKNOWN_TYPE } from '../../src/script/utils'
1313
import ts from 'typescript'
14+
1415
registerTS(() => ts)
1516

1617
describe('resolveType', () => {
@@ -128,7 +129,7 @@ describe('resolveType', () => {
128129
defineProps<{ self: any } & Foo & Bar & Baz>()
129130
`).props,
130131
).toStrictEqual({
131-
self: ['Unknown'],
132+
self: [UNKNOWN_TYPE],
132133
foo: ['Number'],
133134
// both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
134135
// preferred
@@ -455,13 +456,13 @@ describe('resolveType', () => {
455456
const { props } = resolve(
456457
`
457458
import { IMP } from './foo'
458-
interface Foo { foo: 1, ${1}: 1 }
459+
interface Foo { foo: 1, ${1}: 1 }
459460
type Bar = { bar: 1 }
460461
declare const obj: Bar
461462
declare const set: Set<any>
462463
declare const arr: Array<any>
463464
464-
defineProps<{
465+
defineProps<{
465466
imp: keyof IMP,
466467
foo: keyof Foo,
467468
bar: keyof Bar,
@@ -483,6 +484,81 @@ describe('resolveType', () => {
483484
})
484485
})
485486

487+
test('keyof: index signature', () => {
488+
const { props } = resolve(
489+
`
490+
declare const num: number;
491+
interface Foo {
492+
[key: symbol]: 1
493+
[key: string]: 1
494+
[key: typeof num]: 1,
495+
}
496+
497+
type Test<T> = T
498+
type Bar = {
499+
[key: string]: 1
500+
[key: Test<number>]: 1
501+
}
502+
503+
defineProps<{
504+
foo: keyof Foo
505+
bar: keyof Bar
506+
}>()
507+
`,
508+
)
509+
510+
expect(props).toStrictEqual({
511+
foo: ['Symbol', 'String', 'Number'],
512+
bar: [UNKNOWN_TYPE],
513+
})
514+
})
515+
516+
test('keyof: utility type', () => {
517+
const { props } = resolve(
518+
`
519+
type Foo = Record<symbol | string, any>
520+
type Bar = { [key: string]: any }
521+
type AnyRecord = Record<keyof any, any>
522+
type Baz = { a: 1, ${1}: 2, b: 3}
523+
524+
defineProps<{
525+
record: keyof Foo,
526+
anyRecord: keyof AnyRecord
527+
partial: keyof Partial<Bar>,
528+
required: keyof Required<Bar>,
529+
readonly: keyof Readonly<Bar>,
530+
pick: keyof Pick<Baz, 'a' | 1>
531+
extract: keyof Extract<keyof Baz, 'a' | 1>
532+
}>()
533+
`,
534+
)
535+
536+
expect(props).toStrictEqual({
537+
record: ['Symbol', 'String'],
538+
anyRecord: ['String', 'Number', 'Symbol'],
539+
partial: ['String'],
540+
required: ['String'],
541+
readonly: ['String'],
542+
pick: ['String', 'Number'],
543+
extract: ['String', 'Number'],
544+
})
545+
})
546+
547+
test('keyof: fallback to Unknown', () => {
548+
const { props } = resolve(
549+
`
550+
interface Barr {}
551+
interface Bar extends Barr {}
552+
type Foo = keyof Bar
553+
defineProps<{ foo: Foo }>()
554+
`,
555+
)
556+
557+
expect(props).toStrictEqual({
558+
foo: [UNKNOWN_TYPE],
559+
})
560+
})
561+
486562
test('ExtractPropTypes (element-plus)', () => {
487563
const { props, raw } = resolve(
488564
`

packages/compiler-sfc/src/script/resolveType.ts

Lines changed: 133 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,17 @@ export function inferRuntimeType(
14761476
m.key.type === 'NumericLiteral'
14771477
) {
14781478
types.add('Number')
1479+
} else if (m.type === 'TSIndexSignature') {
1480+
const annotation = m.parameters[0].typeAnnotation
1481+
if (annotation && annotation.type !== 'Noop') {
1482+
const type = inferRuntimeType(
1483+
ctx,
1484+
annotation.typeAnnotation,
1485+
scope,
1486+
)[0]
1487+
if (type === UNKNOWN_TYPE) return [UNKNOWN_TYPE]
1488+
types.add(type)
1489+
}
14791490
} else {
14801491
types.add('String')
14811492
}
@@ -1489,7 +1500,9 @@ export function inferRuntimeType(
14891500
}
14901501
}
14911502

1492-
return types.size ? Array.from(types) : ['Object']
1503+
return types.size
1504+
? Array.from(types)
1505+
: [isKeyOf ? UNKNOWN_TYPE : 'Object']
14931506
}
14941507
case 'TSPropertySignature':
14951508
if (node.typeAnnotation) {
@@ -1533,81 +1546,123 @@ export function inferRuntimeType(
15331546
case 'String':
15341547
case 'Array':
15351548
case 'ArrayLike':
1549+
case 'Parameters':
1550+
case 'ConstructorParameters':
15361551
case 'ReadonlyArray':
15371552
return ['String', 'Number']
1538-
default:
1553+
1554+
// TS built-in utility types
1555+
case 'Record':
1556+
case 'Partial':
1557+
case 'Required':
1558+
case 'Readonly':
1559+
if (node.typeParameters && node.typeParameters.params[0]) {
1560+
return inferRuntimeType(
1561+
ctx,
1562+
node.typeParameters.params[0],
1563+
scope,
1564+
true,
1565+
)
1566+
}
1567+
break
1568+
case 'Pick':
1569+
case 'Extract':
1570+
if (node.typeParameters && node.typeParameters.params[1]) {
1571+
return inferRuntimeType(
1572+
ctx,
1573+
node.typeParameters.params[1],
1574+
scope,
1575+
)
1576+
}
1577+
break
1578+
1579+
case 'Function':
1580+
case 'Object':
1581+
case 'Set':
1582+
case 'Map':
1583+
case 'WeakSet':
1584+
case 'WeakMap':
1585+
case 'Date':
1586+
case 'Promise':
1587+
case 'Error':
1588+
case 'Uppercase':
1589+
case 'Lowercase':
1590+
case 'Capitalize':
1591+
case 'Uncapitalize':
1592+
case 'ReadonlyMap':
1593+
case 'ReadonlySet':
15391594
return ['String']
15401595
}
1541-
}
1596+
} else {
1597+
switch (node.typeName.name) {
1598+
case 'Array':
1599+
case 'Function':
1600+
case 'Object':
1601+
case 'Set':
1602+
case 'Map':
1603+
case 'WeakSet':
1604+
case 'WeakMap':
1605+
case 'Date':
1606+
case 'Promise':
1607+
case 'Error':
1608+
return [node.typeName.name]
1609+
1610+
// TS built-in utility types
1611+
// https://www.typescriptlang.org/docs/handbook/utility-types.html
1612+
case 'Partial':
1613+
case 'Required':
1614+
case 'Readonly':
1615+
case 'Record':
1616+
case 'Pick':
1617+
case 'Omit':
1618+
case 'InstanceType':
1619+
return ['Object']
1620+
1621+
case 'Uppercase':
1622+
case 'Lowercase':
1623+
case 'Capitalize':
1624+
case 'Uncapitalize':
1625+
return ['String']
15421626

1543-
switch (node.typeName.name) {
1544-
case 'Array':
1545-
case 'Function':
1546-
case 'Object':
1547-
case 'Set':
1548-
case 'Map':
1549-
case 'WeakSet':
1550-
case 'WeakMap':
1551-
case 'Date':
1552-
case 'Promise':
1553-
case 'Error':
1554-
return [node.typeName.name]
1555-
1556-
// TS built-in utility types
1557-
// https://www.typescriptlang.org/docs/handbook/utility-types.html
1558-
case 'Partial':
1559-
case 'Required':
1560-
case 'Readonly':
1561-
case 'Record':
1562-
case 'Pick':
1563-
case 'Omit':
1564-
case 'InstanceType':
1565-
return ['Object']
1566-
1567-
case 'Uppercase':
1568-
case 'Lowercase':
1569-
case 'Capitalize':
1570-
case 'Uncapitalize':
1571-
return ['String']
1572-
1573-
case 'Parameters':
1574-
case 'ConstructorParameters':
1575-
case 'ReadonlyArray':
1576-
return ['Array']
1577-
1578-
case 'ReadonlyMap':
1579-
return ['Map']
1580-
case 'ReadonlySet':
1581-
return ['Set']
1582-
1583-
case 'NonNullable':
1584-
if (node.typeParameters && node.typeParameters.params[0]) {
1585-
return inferRuntimeType(
1586-
ctx,
1587-
node.typeParameters.params[0],
1588-
scope,
1589-
).filter(t => t !== 'null')
1590-
}
1591-
break
1592-
case 'Extract':
1593-
if (node.typeParameters && node.typeParameters.params[1]) {
1594-
return inferRuntimeType(
1595-
ctx,
1596-
node.typeParameters.params[1],
1597-
scope,
1598-
)
1599-
}
1600-
break
1601-
case 'Exclude':
1602-
case 'OmitThisParameter':
1603-
if (node.typeParameters && node.typeParameters.params[0]) {
1604-
return inferRuntimeType(
1605-
ctx,
1606-
node.typeParameters.params[0],
1607-
scope,
1608-
)
1609-
}
1610-
break
1627+
case 'Parameters':
1628+
case 'ConstructorParameters':
1629+
case 'ReadonlyArray':
1630+
return ['Array']
1631+
1632+
case 'ReadonlyMap':
1633+
return ['Map']
1634+
case 'ReadonlySet':
1635+
return ['Set']
1636+
1637+
case 'NonNullable':
1638+
if (node.typeParameters && node.typeParameters.params[0]) {
1639+
return inferRuntimeType(
1640+
ctx,
1641+
node.typeParameters.params[0],
1642+
scope,
1643+
).filter(t => t !== 'null')
1644+
}
1645+
break
1646+
case 'Extract':
1647+
if (node.typeParameters && node.typeParameters.params[1]) {
1648+
return inferRuntimeType(
1649+
ctx,
1650+
node.typeParameters.params[1],
1651+
scope,
1652+
)
1653+
}
1654+
break
1655+
case 'Exclude':
1656+
case 'OmitThisParameter':
1657+
if (node.typeParameters && node.typeParameters.params[0]) {
1658+
return inferRuntimeType(
1659+
ctx,
1660+
node.typeParameters.params[0],
1661+
scope,
1662+
)
1663+
}
1664+
break
1665+
}
16111666
}
16121667
}
16131668
// cannot infer, fallback to UNKNOWN: ThisParameterType
@@ -1674,6 +1729,13 @@ export function inferRuntimeType(
16741729
node.operator === 'keyof',
16751730
)
16761731
}
1732+
1733+
case 'TSAnyKeyword': {
1734+
if (isKeyOf) {
1735+
return ['String', 'Number', 'Symbol']
1736+
}
1737+
break
1738+
}
16771739
}
16781740
} catch (e) {
16791741
// always soft fail on failed runtime type inference

0 commit comments

Comments
 (0)