diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index 9b62fed63b..1184b57041 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -1,7 +1,9 @@ +import { camelize } from '@vue/shared'; import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; import type { Code, Sfc, TextRange } from '../../types'; import { codeFeatures } from '../codeFeatures'; import { combineLastMapping, endOfLine, generateSfcBlockSection, newLine } from '../utils'; +import { generateCamelized } from '../utils/camelized'; import { generateComponent, generateEmitsOption } from './component'; import { generateComponentSelf } from './componentSelf'; import type { ScriptCodegenContext } from './context'; @@ -477,7 +479,7 @@ function* generateComponentProps( } yield `: `; - yield getRangeName(scriptSetup, defineProp.defaultValue); + yield getRangeText(scriptSetup, defineProp.defaultValue); yield `,${newLine}`; } yield `}${endOfLine}`; @@ -491,6 +493,13 @@ function* generateComponentProps( ctx.generatedPropsType = true; yield `${ctx.localTypes.PropsChildren}<__VLS_Slots>`; } + if (scriptSetupRanges.defineProps?.typeArg) { + if (ctx.generatedPropsType) { + yield ` & `; + } + ctx.generatedPropsType = true; + yield `__VLS_Props`; + } if (scriptSetupRanges.defineProp.length) { if (ctx.generatedPropsType) { yield ` & `; @@ -504,11 +513,17 @@ function* generateComponentProps( yield generateSfcBlockSection(scriptSetup, defineProp.comments.start, defineProp.comments.end, codeFeatures.all); yield newLine; } + if (defineProp.isModel && !defineProp.name) { yield propName!; } else if (defineProp.name) { - yield generateSfcBlockSection(scriptSetup, defineProp.name.start, defineProp.name.end, codeFeatures.navigation); + yield* generateCamelized( + getRangeText(scriptSetup, defineProp.name), + scriptSetup.name, + defineProp.name.start, + codeFeatures.navigation + ); } else if (defineProp.localName) { yield generateSfcBlockSection(scriptSetup, defineProp.localName.start, defineProp.localName.end, codeFeatures.navigation); @@ -525,19 +540,12 @@ function* generateComponentProps( if (defineProp.modifierType) { const modifierName = `${defineProp.name ? propName : 'model'}Modifiers`; - const modifierType = getRangeName(scriptSetup, defineProp.modifierType); + const modifierType = getRangeText(scriptSetup, defineProp.modifierType); yield `'${modifierName}'?: Partial>,${newLine}`; } } yield `}`; } - if (scriptSetupRanges.defineProps?.typeArg) { - if (ctx.generatedPropsType) { - yield ` & `; - } - ctx.generatedPropsType = true; - yield `__VLS_Props`; - } if (!ctx.generatedPropsType) { yield `{}`; } @@ -555,7 +563,7 @@ function* generateModelEmit( const [propName, localName] = getPropAndLocalName(scriptSetup, defineModel); yield `'update:${propName}': [value: `; yield* generateDefinePropType(scriptSetup, propName, localName, defineModel); - if (!defineModel.required && defineModel.defaultValue === undefined) { + if (!defineModel.required && !defineModel.defaultValue) { yield ` | undefined`; } yield `]${endOfLine}`; @@ -573,7 +581,7 @@ function* generateDefinePropType( ) { if (defineProp.type) { // Infer from defineProp - yield getRangeName(scriptSetup, defineProp.type); + yield getRangeText(scriptSetup, defineProp.type); } else if (defineProp.runtimeType && localName) { // Infer from actual prop declaration code @@ -593,20 +601,17 @@ function getPropAndLocalName( defineProp: ScriptSetupRanges['defineProp'][number] ) { const localName = defineProp.localName - ? getRangeName(scriptSetup, defineProp.localName) + ? getRangeText(scriptSetup, defineProp.localName) : undefined; - let propName = defineProp.name - ? getRangeName(scriptSetup, defineProp.name) + const propName = defineProp.name + ? camelize(getRangeText(scriptSetup, defineProp.name).slice(1, -1)) : defineProp.isModel ? 'modelValue' : localName; - if (defineProp.name) { - propName = propName!.replace(/['"]+/g, ''); - } return [propName, localName] as const; } -function getRangeName( +function getRangeText( scriptSetup: NonNullable, range: TextRange ) { diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index 38aba2e045..de3497225b 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -3,7 +3,7 @@ import { camelize, capitalize } from '@vue/shared'; import type { Code, VueCodeInformation } from '../../types'; import { getSlotsPropertyName, hyphenateTag } from '../../utils/shared'; import { createVBindShorthandInlayHintInfo } from '../inlayHints'; -import { endOfLine, newLine, normalizeAttributeValue, variableNameRegex, wrapWith } from '../utils'; +import { endOfLine, identifierRegex, newLine, normalizeAttributeValue, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import type { TemplateCodegenContext } from './context'; import { generateElementChildren } from './elementChildren'; @@ -99,6 +99,7 @@ export function* generateComponent( const shouldCapitalize = matchImportName[0].toUpperCase() === matchImportName[0]; yield* generateCamelized( shouldCapitalize ? capitalize(node.tag) : node.tag, + 'template', tagOffset, { ...ctx.codeFeatures.withoutHighlightAndCompletion, @@ -164,7 +165,7 @@ export function* generateComponent( yield `${endOfLine}`; const camelizedTag = camelize(node.tag); - if (variableNameRegex.test(camelizedTag)) { + if (identifierRegex.test(camelizedTag)) { // navigation support yield `/** @type {[`; for (const tagOffset of tagOffsets) { @@ -173,6 +174,7 @@ export function* generateComponent( yield `typeof __VLS_components.`; yield* generateCamelized( shouldCapitalize ? capitalize(node.tag) : node.tag, + 'template', tagOffset, { navigation: { @@ -190,6 +192,7 @@ export function* generateComponent( yield `// @ts-ignore${newLine}`; // #2304 yield* generateCamelized( capitalize(node.tag), + 'template', tagOffsets[0], { completion: { @@ -403,7 +406,7 @@ function* generateFailedPropExps( } function getCanonicalComponentName(tagText: string) { - return variableNameRegex.test(tagText) + return identifierRegex.test(tagText) ? tagText : capitalize(camelize(tagText.replace(colonReg, '-'))); } @@ -423,12 +426,13 @@ function getPossibleOriginalComponentNames(tagText: string, deduplicate: boolean } function* generateCanonicalComponentName(tagText: string, offset: number, features: VueCodeInformation): Generator { - if (variableNameRegex.test(tagText)) { + if (identifierRegex.test(tagText)) { yield [tagText, 'template', offset, features]; } else { yield* generateCamelized( capitalize(tagText.replace(colonReg, '-')), + 'template', offset, features ); @@ -482,7 +486,7 @@ function* generateElementReference( ); yield `} */${endOfLine}`; - if (variableNameRegex.test(content) && !options.templateRefNames.has(content)) { + if (identifierRegex.test(content) && !options.templateRefNames.has(content)) { ctx.accessExternalVariable(content, startOffset); } diff --git a/packages/language-core/lib/codegen/template/elementDirectives.ts b/packages/language-core/lib/codegen/template/elementDirectives.ts index 94630b05b3..71448f9f53 100644 --- a/packages/language-core/lib/codegen/template/elementDirectives.ts +++ b/packages/language-core/lib/codegen/template/elementDirectives.ts @@ -69,6 +69,7 @@ function* generateIdentifier( `__VLS_directives.`, ...generateCamelized( rawName, + 'template', prop.loc.start.offset, ctx.resolveCodeFeatures({ ...codeFeatures.withoutHighlight, diff --git a/packages/language-core/lib/codegen/template/elementEvents.ts b/packages/language-core/lib/codegen/template/elementEvents.ts index 97ce5f20ac..a3af4da972 100644 --- a/packages/language-core/lib/codegen/template/elementEvents.ts +++ b/packages/language-core/lib/codegen/template/elementEvents.ts @@ -2,7 +2,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import type * as ts from 'typescript'; import type { Code } from '../../types'; -import { combineLastMapping, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../utils'; +import { combineLastMapping, createTsAst, endOfLine, identifierRegex, newLine, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import type { TemplateCodegenContext } from './context'; import type { TemplateCodegenOptions } from './index'; @@ -65,11 +65,12 @@ export function* generateEventArg( ...ctx.codeFeatures.withoutHighlightAndCompletion, ...ctx.codeFeatures.navigationWithoutRename, }; - if (variableNameRegex.test(camelize(name))) { + if (identifierRegex.test(camelize(name))) { yield ['', 'template', start, features]; yield directive; yield* generateCamelized( capitalize(name), + 'template', start, combineLastMapping ); @@ -84,6 +85,7 @@ export function* generateEventArg( directive, ...generateCamelized( capitalize(name), + 'template', start, combineLastMapping ), diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts index 4ee4906c84..80aaef1a18 100644 --- a/packages/language-core/lib/codegen/template/elementProps.ts +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -6,7 +6,7 @@ import type { Code, VueCodeInformation, VueCompilerOptions } from '../../types'; import { hyphenateAttr, hyphenateTag } from '../../utils/shared'; import { codeFeatures } from '../codeFeatures'; import { createVBindShorthandInlayHintInfo } from '../inlayHints'; -import { newLine, variableNameRegex, wrapWith } from '../utils'; +import { identifierRegex, newLine, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import { generateUnicode } from '../utils/unicode'; import type { TemplateCodegenContext } from './context'; @@ -303,12 +303,13 @@ export function* generatePropExp( else { const propVariableName = camelize(exp.loc.source); - if (variableNameRegex.test(propVariableName)) { + if (identifierRegex.test(propVariableName)) { const isDestructuredProp = options.destructuredPropNames?.has(propVariableName) ?? false; const isTemplateRef = options.templateRefNames?.has(propVariableName) ?? false; const codes = generateCamelized( exp.loc.source, + 'template', exp.loc.start.offset, features ); diff --git a/packages/language-core/lib/codegen/template/objectProperty.ts b/packages/language-core/lib/codegen/template/objectProperty.ts index 78df1a85c6..0bdf9facae 100644 --- a/packages/language-core/lib/codegen/template/objectProperty.ts +++ b/packages/language-core/lib/codegen/template/objectProperty.ts @@ -1,6 +1,6 @@ import { camelize } from '@vue/shared'; import type { Code, VueCodeInformation } from '../../types'; -import { combineLastMapping, variableNameRegex, wrapWith } from '../utils'; +import { combineLastMapping, identifierRegex, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import { generateStringLiteralKey } from '../utils/stringLiteralKey'; import type { TemplateCodegenContext } from './context'; @@ -44,8 +44,8 @@ export function* generateObjectProperty( } } else if (shouldCamelize) { - if (variableNameRegex.test(camelize(code))) { - yield* generateCamelized(code, offset, features); + if (identifierRegex.test(camelize(code))) { + yield* generateCamelized(code, 'template', offset, features); } else { yield* wrapWith( @@ -53,13 +53,13 @@ export function* generateObjectProperty( offset + code.length, features, `'`, - ...generateCamelized(code, offset, combineLastMapping), + ...generateCamelized(code, 'template', offset, combineLastMapping), `'` ); } } else { - if (variableNameRegex.test(code)) { + if (identifierRegex.test(code)) { yield [code, 'template', offset, features]; } else { diff --git a/packages/language-core/lib/codegen/template/propertyAccess.ts b/packages/language-core/lib/codegen/template/propertyAccess.ts index 0733d38b01..3c9a5fedd1 100644 --- a/packages/language-core/lib/codegen/template/propertyAccess.ts +++ b/packages/language-core/lib/codegen/template/propertyAccess.ts @@ -1,5 +1,5 @@ import type { Code, VueCodeInformation } from '../../types'; -import { variableNameRegex } from '../utils'; +import { identifierRegex } from '../utils'; import { generateStringLiteralKey } from '../utils/stringLiteralKey'; import type { TemplateCodegenContext } from './context'; import type { TemplateCodegenOptions } from './index'; @@ -13,7 +13,7 @@ export function* generatePropertyAccess( features?: VueCodeInformation, astHolder?: any ): Generator { - if (!options.compilerOptions.noPropertyAccessFromIndexSignature && variableNameRegex.test(code)) { + if (!options.compilerOptions.noPropertyAccessFromIndexSignature && identifierRegex.test(code)) { yield `.`; yield offset !== undefined && features ? [code, 'template', offset, features] diff --git a/packages/language-core/lib/codegen/utils/camelized.ts b/packages/language-core/lib/codegen/utils/camelized.ts index 5adf5e66b6..1d8ca4e84a 100644 --- a/packages/language-core/lib/codegen/utils/camelized.ts +++ b/packages/language-core/lib/codegen/utils/camelized.ts @@ -1,7 +1,12 @@ import { capitalize } from '@vue/shared'; import type { Code, VueCodeInformation } from '../../types'; -export function* generateCamelized(code: string, offset: number, info: VueCodeInformation): Generator { +export function* generateCamelized( + code: string, + source: string, + offset: number, + info: VueCodeInformation +): Generator { const parts = code.split('-'); for (let i = 0; i < parts.length; i++) { const part = parts[i]; @@ -9,7 +14,7 @@ export function* generateCamelized(code: string, offset: number, info: VueCodeIn if (i === 0) { yield [ part, - 'template', + source, offset, info, ]; @@ -17,7 +22,7 @@ export function* generateCamelized(code: string, offset: number, info: VueCodeIn else { yield [ capitalize(part), - 'template', + source, offset, { __combineOffset: i }, ]; diff --git a/packages/language-core/lib/codegen/utils/index.ts b/packages/language-core/lib/codegen/utils/index.ts index 7bdac7e272..0684ef6c3a 100644 --- a/packages/language-core/lib/codegen/utils/index.ts +++ b/packages/language-core/lib/codegen/utils/index.ts @@ -6,7 +6,7 @@ import type { Code, SfcBlock, SfcBlockAttr, VueCodeInformation } from '../../typ export const newLine = `\n`; export const endOfLine = `;${newLine}`; export const combineLastMapping: VueCodeInformation = { __combineOffset: 1 }; -export const variableNameRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; +export const identifierRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; export function* wrapWith( startOffset: number, diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index 01e0615c35..1d3dbf7ae4 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -153,10 +153,16 @@ export function parseScriptSetupRanges( && ts.isIdentifier(node.expression) ) { const callText = _getNodeText(node.expression); - if (vueCompilerOptions.macros.defineModel.includes(callText)) { + const isDefineModel = vueCompilerOptions.macros.defineModel.includes(callText); + if (isDefineModel || callText === 'defineProp') { let localName: TextRange | undefined; - let propName: TextRange | undefined; + let propName: ts.Expression | undefined; let options: ts.Expression | undefined; + let type: TextRange | undefined; + let modifierType: TextRange | undefined; + let runtimeType: TextRange | undefined; + let defaultValue: TextRange | undefined; + let required = false; if ( ts.isVariableDeclaration(parent) && @@ -165,91 +171,35 @@ export function parseScriptSetupRanges( localName = _getStartEnd(parent.name); } - if (node.arguments.length >= 2) { - propName = _getStartEnd(node.arguments[0]); - options = node.arguments[1]; - } - else if (node.arguments.length >= 1) { - if (ts.isStringLiteralLike(node.arguments[0])) { - propName = _getStartEnd(node.arguments[0]); + if (node.typeArguments) { + if (node.typeArguments.length >= 1) { + type = _getStartEnd(node.typeArguments[0]); } - else { - options = node.arguments[0]; + if (node.typeArguments.length >= 2) { + modifierType = _getStartEnd(node.typeArguments[1]); } } - let runtimeType: TextRange | undefined; - let defaultValue: TextRange | undefined; - let required = false; - if (options && ts.isObjectLiteralExpression(options)) { - for (const property of options.properties) { - if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) { - continue; - } - const text = _getNodeText(property.name); - if (text === 'type') { - runtimeType = _getStartEnd(property.initializer); - } - else if (text === 'default') { - defaultValue = _getStartEnd(property.initializer); + if (isDefineModel) { + if (node.arguments.length >= 2) { + propName = node.arguments[0]; + options = node.arguments[1]; + } + else if (node.arguments.length >= 1) { + if (ts.isStringLiteralLike(node.arguments[0])) { + propName = node.arguments[0]; } - else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) { - required = true; + else { + options = node.arguments[0]; } } } - defineProp.push({ - localName, - name: propName, - type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined, - modifierType: node.typeArguments && node.typeArguments?.length >= 2 ? _getStartEnd(node.typeArguments[1]) : undefined, - runtimeType, - defaultValue, - required, - isModel: true, - comments: getCommentsRange(ts, node, parents, ast), - argNode: options, - }); - } - else if (callText === 'defineProp') { - let localName: TextRange | undefined; - let propName: TextRange | undefined; - let options: ts.Expression | undefined; - - if ( - ts.isVariableDeclaration(parent) && - ts.isIdentifier(parent.name) - ) { - localName = _getStartEnd(parent.name); - } - - let runtimeType: TextRange | undefined; - let defaultValue: TextRange | undefined; - let required = false; - if (definePropProposalA) { + else if (definePropProposalA) { if (node.arguments.length >= 2) { options = node.arguments[1]; } if (node.arguments.length >= 1) { - propName = _getStartEnd(node.arguments[0]); - } - - if (options && ts.isObjectLiteralExpression(options)) { - for (const property of options.properties) { - if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) { - continue; - } - const text = _getNodeText(property.name); - if (text === 'type') { - runtimeType = _getStartEnd(property.initializer); - } - else if (text === 'default') { - defaultValue = _getStartEnd(property.initializer); - } - else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) { - required = true; - } - } + propName = node.arguments[0]; } } else if (definePropProposalB) { @@ -264,27 +214,40 @@ export function parseScriptSetupRanges( if (node.arguments.length >= 1) { defaultValue = _getStartEnd(node.arguments[0]); } + } - if (options && ts.isObjectLiteralExpression(options)) { - for (const property of options.properties) { - if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) { - continue; - } - const text = _getNodeText(property.name); - if (text === 'type') { - runtimeType = _getStartEnd(property.initializer); - } + if (options && ts.isObjectLiteralExpression(options)) { + for (const property of options.properties) { + if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) { + continue; + } + const text = _getNodeText(property.name); + if (text === 'type') { + runtimeType = _getStartEnd(property.initializer); + } + else if (text === 'default') { + defaultValue = _getStartEnd(property.initializer); + } + else if (text === 'required' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) { + required = true; } } } + let name: TextRange | undefined; + if (propName && ts.isStringLiteralLike(propName)) { + name = _getStartEnd(propName); + } + defineProp.push({ localName, - name: propName, - type: node.typeArguments?.length ? _getStartEnd(node.typeArguments[0]) : undefined, + name, + type, + modifierType, runtimeType, defaultValue, required, + isModel: isDefineModel, comments: getCommentsRange(ts, node, parents, ast), argNode: options, }); diff --git a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap index 74bcbc7840..23f9484a8d 100644 --- a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap +++ b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap @@ -90,9 +90,9 @@ exports[`vue-tsc-dts > Input: generic/component.vue, Output: generic/component.v readonly "onUpdate:title"?: (value: string) => any; readonly onBar?: (data: number) => any; } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:title" | "onBar"> & ({ - title?: string; - } & { foo: number; + } & { + title?: string; }) & Partial<{}>> & import("vue").PublicProps; expose(exposed: import("vue").ShallowUnwrapRef<{ baz: number; @@ -120,9 +120,9 @@ exports[`vue-tsc-dts > Input: generic/custom-extension-component.cext, Output: g readonly "onUpdate:title"?: (value: string) => any; readonly onBar?: (data: number) => any; } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:title" | "onBar"> & ({ - title?: string; - } & { foo: number; + } & { + title?: string; }) & Partial<{}>> & import("vue").PublicProps; expose(exposed: import("vue").ShallowUnwrapRef<{ baz: number; diff --git a/test-workspace/tsc/passedFixtures/vue3/defineModel/main.vue b/test-workspace/tsc/passedFixtures/vue3/defineModel/main.vue index 749963ce8c..bb25b68577 100644 --- a/test-workspace/tsc/passedFixtures/vue3/defineModel/main.vue +++ b/test-workspace/tsc/passedFixtures/vue3/defineModel/main.vue @@ -10,7 +10,7 @@ const ScriptSetupExact = defineComponent({ 'd'?: number, 'e': string, 'f'?: string, - 'g'?: string, + 'gG'?: string, }, __typeEmits: {} as { 'update:modelValue': [modelValue: string | undefined]; @@ -18,7 +18,7 @@ const ScriptSetupExact = defineComponent({ 'update:d': [d: number | undefined]; 'update:e': [e: string]; 'update:f': [f: string | undefined]; - 'update:g': [g: string | undefined]; + 'update:gG': [g: string | undefined]; }, setup() { return {}; @@ -37,6 +37,6 @@ exactType(ScriptSetup, ScriptSetupExact); @update:d="(x) => exactType(x, {} as number | undefined)" @update:e="(x) => exactType(x, {} as string)" @update:f="(x) => exactType(x, {} as string | undefined)" - @update:g="(x) => exactType(x, {} as string | undefined)" + @update:g-g="(x) => exactType(x, {} as string | undefined)" /> diff --git a/test-workspace/tsc/passedFixtures/vue3/defineModel/script-setup.vue b/test-workspace/tsc/passedFixtures/vue3/defineModel/script-setup.vue index 4eb409804a..7a93bcfc75 100644 --- a/test-workspace/tsc/passedFixtures/vue3/defineModel/script-setup.vue +++ b/test-workspace/tsc/passedFixtures/vue3/defineModel/script-setup.vue @@ -7,7 +7,7 @@ const c = defineModel('c', { required: true }); const d = defineModel('d', { required: false }); const e = defineModel('e', { required: true }); const f = defineModel('f', { required: false }); -const g = defineModel('g'); +const g = defineModel('g-g'); exactType(a.value, {} as string | undefined); // exactType(b.value, {} as string); @@ -25,5 +25,5 @@ exactType(g.value, {} as string | undefined); {{ exactType($props.d, {} as number | undefined) }} {{ exactType($props.e, {} as string) }} {{ exactType($props.f, {} as string | undefined) }} - {{ exactType($props.g, {} as string | undefined) }} + {{ exactType($props.gG, {} as string | undefined) }}