diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index b7a13e56851..df4fc952bf9 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -1712,7 +1712,10 @@ export default /*#__PURE__*/_defineComponent({ literalUnionMixed: { type: [String, Number, Boolean], required: true }, intersection: { type: Object, required: true }, intersection2: { type: String, required: true }, - foo: { type: [Function, null], required: true } + foo: { type: [Function, null], required: true }, + unknown: { type: null, required: true }, + unknownUnion: { type: null, required: true }, + unknownIntersection: { type: Object, required: true } }, setup(__props: any, { expose }) { expose(); diff --git a/packages/compiler-sfc/__tests__/compileScript.spec.ts b/packages/compiler-sfc/__tests__/compileScript.spec.ts index 8f96ecc11c5..f375f51ac07 100644 --- a/packages/compiler-sfc/__tests__/compileScript.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript.spec.ts @@ -1036,6 +1036,10 @@ const emit = defineEmits(['a', 'b']) intersection: Test & {} intersection2: 'foo' & ('foo' | 'bar') foo: ((item: any) => boolean) | null + + unknown: UnknownType + unknownUnion: UnknownType | string + unknownIntersection: UnknownType & Object }>() `) assertCode(content) @@ -1082,6 +1086,13 @@ const emit = defineEmits(['a', 'b']) expect(content).toMatch(`intersection: { type: Object, required: true }`) expect(content).toMatch(`intersection2: { type: String, required: true }`) expect(content).toMatch(`foo: { type: [Function, null], required: true }`) + expect(content).toMatch(`unknown: { type: null, required: true }`) + // uninon containing unknown type: skip check + expect(content).toMatch(`unknownUnion: { type: null, required: true }`) + // intersection containing unknown type: narrow to the known types + expect(content).toMatch( + `unknownIntersection: { type: Object, required: true }` + ) expect(bindings).toStrictEqual({ string: BindingTypes.PROPS, number: BindingTypes.PROPS, @@ -1115,7 +1126,10 @@ const emit = defineEmits(['a', 'b']) foo: BindingTypes.PROPS, uppercase: BindingTypes.PROPS, params: BindingTypes.PROPS, - nonNull: BindingTypes.PROPS + nonNull: BindingTypes.PROPS, + unknown: BindingTypes.PROPS, + unknownUnion: BindingTypes.PROPS, + unknownIntersection: BindingTypes.PROPS }) }) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index e2e129924e3..47738d8285b 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -386,7 +386,7 @@ export function compileScript( isFromSetup: boolean, needTemplateUsageCheck: boolean ) { - // template usage check is only needed in non-inline mode, so we can skip + // template usage check is only needed in non-inline mode, so we can UNKNOWN // the work if inlineTemplate is true. let isUsedInTemplate = needTemplateUsageCheck if ( @@ -1100,7 +1100,7 @@ export function compileScript( // check if user has manually specified `name` or 'render` option in // export default - // if has name, skip name inference + // if has name, UNKNOWN name inference // if has render and no template, generate return object instead of // empty render function (#4980) let optionProperties @@ -1403,7 +1403,7 @@ export function compileScript( // 4. extract runtime props/emits code from setup context type if (propsTypeDecl) { - extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd) + extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes) } if (emitsTypeDecl) { extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits) @@ -1576,7 +1576,7 @@ export function compileScript( !userImports[key].source.endsWith('.vue') ) { // generate getter for import bindings - // skip vue imports since we know they will never change + // UNKNOWN vue imports since we know they will never change returned += `get ${key}() { return ${key} }, ` } else if (bindingMetadata[key] === BindingTypes.SETUP_LET) { // local let binding, also add setter @@ -1972,8 +1972,7 @@ function recordType(node: Node, declaredTypes: Record) { function extractRuntimeProps( node: TSTypeLiteral | TSInterfaceBody, props: Record, - declaredTypes: Record, - isProd: boolean + declaredTypes: Record ) { const members = node.type === 'TSTypeLiteral' ? node.members : node.body for (const m of members) { @@ -1981,11 +1980,15 @@ function extractRuntimeProps( (m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') && m.key.type === 'Identifier' ) { - let type + let type: string[] | undefined if (m.type === 'TSMethodSignature') { type = ['Function'] } else if (m.typeAnnotation) { type = inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes) + // skip check for result containing unknown types + if (type.includes(UNKNOWN_TYPE)) { + type = [`null`] + } } props[m.key.name] = { key: m.key.name, @@ -1996,6 +1999,8 @@ function extractRuntimeProps( } } +const UNKNOWN_TYPE = 'Unknown' + function inferRuntimeType( node: TSType, declaredTypes: Record @@ -2009,6 +2014,8 @@ function inferRuntimeType( return ['Boolean'] case 'TSObjectKeyword': return ['Object'] + case 'TSNullKeyword': + return ['null'] case 'TSTypeLiteral': { // TODO (nice to have) generate runtime property validation const types = new Set() @@ -2041,7 +2048,7 @@ function inferRuntimeType( case 'BigIntLiteral': return ['Number'] default: - return [`null`] + return [`UNKNOWN`] } case 'TSTypeReference': @@ -2104,31 +2111,43 @@ function inferRuntimeType( declaredTypes ) } - // cannot infer, fallback to null: ThisParameterType + // cannot infer, fallback to UNKNOWN: ThisParameterType } } - return [`null`] + return [UNKNOWN_TYPE] case 'TSParenthesizedType': return inferRuntimeType(node.typeAnnotation, declaredTypes) + case 'TSUnionType': - case 'TSIntersectionType': - return [ - ...new Set( - [].concat( - ...(node.types.map(t => inferRuntimeType(t, declaredTypes)) as any) - ) - ) - ] + return flattenTypes(node.types, declaredTypes) + case 'TSIntersectionType': { + return flattenTypes(node.types, declaredTypes).filter( + t => t !== UNKNOWN_TYPE + ) + } case 'TSSymbolKeyword': return ['Symbol'] default: - return [`null`] // no runtime check + return [UNKNOWN_TYPE] // no runtime check } } +function flattenTypes( + types: TSType[], + declaredTypes: Record +): string[] { + return [ + ...new Set( + ([] as string[]).concat( + ...types.map(t => inferRuntimeType(t, declaredTypes)) + ) + ) + ] +} + function toRuntimeTypeString(types: string[]) { return types.length > 1 ? `[${types.join(', ')}]` : types[0] }