@@ -91,15 +91,33 @@ export function prepareAndCheckForConversion(
9191 // If the input is using `@Input() bla?: string;` with the "optional question mark",
9292 // then we try to explicitly add `undefined` as type, if it's not part of the type already.
9393 // This is ensuring correctness, as `bla?` automatically includes `undefined` currently.
94- if (
95- node . type !== undefined &&
96- node . questionToken !== undefined &&
97- ! checker . isTypeAssignableTo ( checker . getUndefinedType ( ) , checker . getTypeFromTypeNode ( node . type ) )
98- ) {
99- typeToAdd = ts . factory . createUnionTypeNode ( [
100- node . type ,
101- ts . factory . createKeywordTypeNode ( ts . SyntaxKind . UndefinedKeyword ) ,
102- ] ) ;
94+ if ( node . questionToken !== undefined ) {
95+ // If there is no type, but we have an initial value, try inferring
96+ // it from the initializer.
97+ if ( typeToAdd === undefined && initialValue !== undefined ) {
98+ const inferredType = inferImportableTypeForInput ( checker , node , initialValue ) ;
99+ if ( inferredType !== null ) {
100+ typeToAdd = inferredType ;
101+ }
102+ }
103+ if ( typeToAdd === undefined ) {
104+ return {
105+ context : node ,
106+ reason : InputIncompatibilityReason . InputWithQuestionMarkButNoGoodExplicitTypeExtractable ,
107+ } ;
108+ }
109+
110+ if (
111+ ! checker . isTypeAssignableTo (
112+ checker . getUndefinedType ( ) ,
113+ checker . getTypeFromTypeNode ( typeToAdd ) ,
114+ )
115+ ) {
116+ typeToAdd = ts . factory . createUnionTypeNode ( [
117+ typeToAdd ,
118+ ts . factory . createKeywordTypeNode ( ts . SyntaxKind . UndefinedKeyword ) ,
119+ ] ) ;
120+ }
103121 }
104122
105123 let leadingTodoText : string | null = null ;
@@ -128,25 +146,9 @@ export function prepareAndCheckForConversion(
128146 // Attempt to extract type from input initial value. No explicit type, but input is required.
129147 // Hence we need an explicit type, or fall back to `typeof`.
130148 if ( typeToAdd === undefined && initialValue !== undefined && metadata . required ) {
131- const propertyType = checker . getTypeAtLocation ( node ) ;
132- if ( propertyType . flags & ts . TypeFlags . Boolean ) {
133- typeToAdd = ts . factory . createKeywordTypeNode ( ts . SyntaxKind . BooleanKeyword ) ;
134- } else if ( propertyType . flags & ts . TypeFlags . String ) {
135- typeToAdd = ts . factory . createKeywordTypeNode ( ts . SyntaxKind . StringKeyword ) ;
136- } else if ( propertyType . flags & ts . TypeFlags . Number ) {
137- typeToAdd = ts . factory . createKeywordTypeNode ( ts . SyntaxKind . NumberKeyword ) ;
138- } else if ( ts . isIdentifier ( initialValue ) ) {
139- // @Input ({required: true}) bla = SOME_DEFAULT;
140- typeToAdd = ts . factory . createTypeQueryNode ( initialValue ) ;
141- } else if (
142- ts . isPropertyAccessExpression ( initialValue ) &&
143- ts . isIdentifier ( initialValue . name ) &&
144- ts . isIdentifier ( initialValue . expression )
145- ) {
146- // @Input ({required: true}) bla = prop.SOME_DEFAULT;
147- typeToAdd = ts . factory . createTypeQueryNode (
148- ts . factory . createQualifiedName ( initialValue . name , initialValue . expression ) ,
149- ) ;
149+ const inferredType = inferImportableTypeForInput ( checker , node , initialValue ) ;
150+ if ( inferredType !== null ) {
151+ typeToAdd = inferredType ;
150152 } else {
151153 // Note that we could use `typeToTypeNode` here but it's likely breaking because
152154 // the generated type might depend on imports that we cannot add here (nor want).
@@ -167,3 +169,49 @@ export function prepareAndCheckForConversion(
167169 leadingTodoText,
168170 } ;
169171}
172+
173+ function inferImportableTypeForInput (
174+ checker : ts . TypeChecker ,
175+ node : InputNode ,
176+ initialValue : ts . Node ,
177+ ) : ts . TypeNode | null {
178+ const propertyType = checker . getTypeAtLocation ( node ) ;
179+
180+ // If the resolved type is a primitive, or union of primitive types,
181+ // return a type node fully derived from the resolved type.
182+ if (
183+ isPrimitiveImportableTypeNode ( propertyType ) ||
184+ ( propertyType . isUnion ( ) && propertyType . types . every ( isPrimitiveImportableTypeNode ) )
185+ ) {
186+ return checker . typeToTypeNode ( propertyType , node , ts . NodeBuilderFlags . NoTypeReduction ) ?? null ;
187+ }
188+
189+ // Alternatively, try to infer a simple importable type from\
190+ // the initializer.
191+
192+ if ( ts . isIdentifier ( initialValue ) ) {
193+ // @Input ({required: true}) bla = SOME_DEFAULT;
194+ return ts . factory . createTypeQueryNode ( initialValue ) ;
195+ } else if (
196+ ts . isPropertyAccessExpression ( initialValue ) &&
197+ ts . isIdentifier ( initialValue . name ) &&
198+ ts . isIdentifier ( initialValue . expression )
199+ ) {
200+ // @Input ({required: true}) bla = prop.SOME_DEFAULT;
201+ return ts . factory . createTypeQueryNode (
202+ ts . factory . createQualifiedName ( initialValue . name , initialValue . expression ) ,
203+ ) ;
204+ }
205+
206+ return null ;
207+ }
208+
209+ function isPrimitiveImportableTypeNode ( type : ts . Type ) : boolean {
210+ return ! ! (
211+ type . flags & ts . TypeFlags . BooleanLike ||
212+ type . flags & ts . TypeFlags . StringLike ||
213+ type . flags & ts . TypeFlags . NumberLike ||
214+ type . flags & ts . TypeFlags . Undefined ||
215+ type . flags & ts . TypeFlags . Null
216+ ) ;
217+ }
0 commit comments