From f0cfa00a340800af4670cbfb5604f001e8bbebd0 Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Tue, 30 Nov 2021 14:46:46 -0500 Subject: [PATCH] refactor(forms): Move FormControl to an overridden exported constructor. (#44316) (#44806) This implementation change was originally proposed as part of Typed Forms, and will have major consequences for that project as described in the design doc. Submitting it separately will greatly simplify the risk of landing Typed Forms. This change should have no visible impact on normal users of FormControl. See the Typed Forms design doc here: https://docs.google.com/document/d/1cWuBE-oo5WLtwkLFxbNTiaVQGNk8ipgbekZcKBeyxxo. PR Close #44316 PR Close #44806 --- .../mergeOverriddenImplementation.js | 83 ++--- .../mergeOverriddenImplementation.spec.js | 17 +- goldens/public-api/forms/forms.md | 6 +- packages/forms/src/forms.ts | 3 +- packages/forms/src/model.ts | 310 +++++++++++------- packages/forms/test/form_control_spec.ts | 51 +++ 6 files changed, 286 insertions(+), 184 deletions(-) diff --git a/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.js b/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.js index 6477cbae2f4..c801f18ac6d 100644 --- a/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.js +++ b/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.js @@ -8,27 +8,29 @@ * This looks like the following: * * ``` + * /** + * * @overriddenImplementation FooCtor + * *\/ * export interface Foo { * bar(); * } * - * export class FooImpl { - * bar() {} - * } + * type FooInterface = Foo; + * + * export class FooImpl { ... } * * export interface FooCtor { * new(): Foo; * } * - * export const Foo: FooCtor = FooImpl as FooCtor; + * export const Foo: FooCtor = + (class Foo implements FooInterface { ... } * ``` * - * This processor will correct the docs for symbol `Foo` by copying them over from `FooImpl` - * to the exported symbol `Foo`. The processor will also copy all documented constructor overrides from `FooCtor`. + * This processor will extend the docs for symbol `Foo` by copying all documented constructor overrides from `FooCtor`. * - * In order to use this processor, annotate the exported constant with `@overriddenImplementation`, - * and mark the implementation and constructor types as `@internal`. Place the desired - * documentation on the implementation class. + * In order to use this processor, annotate the exported interface with `@overriddenImplementation`. Place the desired + * documentation on the interface and constructor signatures. */ module.exports = function mergeOverriddenImplementation(getDocFromAlias, log) { return { @@ -40,62 +42,33 @@ ], $process(docs) { docs.forEach(doc => { - if (doc.overriddenImplementation) { - // Check the AST is of the expected expression shape, and extract the identifiers. - const symbolAstObjects = [doc.declaration?.name, doc.declaration?.type, doc.declaration?.initializer?.expression]; - if (symbolAstObjects.some(symbol => symbol === undefined)) { - throw new Error('@overriddenImplementation must have format `export const Foo: FooCtor = FooImpl as FooCtor;`'); - } - - // Convert the AST nodes into docs. - const symbolNames = symbolAstObjects.map(s => s.getText()); - const symbolDocArrays = symbolNames.map(symbol => getDocFromAlias(symbol)); - for (let i = 0; i < symbolDocArrays.length; i++) { - if (symbolDocArrays[i].length === 0) { - throw new Error(`@overriddenImplementation failed to find a doc for ${symbolNames[i]}. Are you sure this symbol is documented and exported?`); + if (doc.overriddenImplementation) { + // Convert the specified name into a doc. + const ctorDocArray = getDocFromAlias(doc.overriddenImplementation); + if (ctorDocArray.length === 0) { + throw new Error(`@overriddenImplementation failed to find a doc for ${doc.overriddenImplementation}. Are you sure this symbol is documented and exported?`); } - if (symbolDocArrays[i].length >= 2) { - throw new Error(`@overriddenImplementation found multiple docs for ${symbolNames[i]}. You may only have one documented symbol for each.`); + if (ctorDocArray.length >= 2) { + throw new Error(`@overriddenImplementation found multiple docs for ${doc.overriddenImplementation}. You may only have one documented symbol for each.`); } - } - const symbolDocs = symbolDocArrays.map(a => a[0]); - const exportedNameDoc = symbolDocs[0]; - const ctorDoc = symbolDocs[1]; - const implDoc = symbolDocs[2]; - - // Clean out the unwanted properties from the exported doc. - Object.keys(doc).forEach(key => { - if (!this.propertiesToKeep.includes(key)) { - delete doc[key]; - } - }); - - // Copy over all the properties from the implementation doc. - Object.keys(implDoc).forEach(key => { - if (!this.propertiesToKeep.includes(key)) { - exportedNameDoc[key] = implDoc[key]; - } - }); + + const ctorDoc = ctorDocArray[0]; // Copy the constructor overrides from the constructor doc, if any are present. - if (!ctorDoc.members || ctorDoc.members.length !== 1 || !ctorDoc.members[0].name.includes('new')) { - throw new Error(`@overriddenImplementation requires that the provided constructor ${symbolNames[1]} have exactly one member called "new", possibly with multiple overrides.`); + if (!ctorDoc.members || ctorDoc.members.length === 0 || !ctorDoc.members[0].name.includes('new')) { + throw new Error(`@overriddenImplementation requires that the provided constructor ${ctorDoc} have a member called "new", possibly with multiple overrides.`); } - exportedNameDoc.constructorDoc = ctorDoc.members[0]; + doc.constructorDoc = ctorDoc.members[0]; - // Mark symbols other than the exported name as internal. + // Mark the constructor doc internal. if (!ctorDoc.internal) { - log.warn(`Constructor doc ${symbolNames[1]} was not marked '@internal'; adding this annotation.`); + log.warn(`Constructor doc ${ctorDoc} was not marked '@internal'; adding this annotation.`); ctorDoc.internal = true; } - if (!implDoc.internal) { - log.warn(`Implementation doc ${symbolNames[2]} was not marked '@internal'; adding this annotation.`); - implDoc.internal = true; - } - // The exported doc should not be private, unlike the implementation doc. - exportedNameDoc.privateExport = false; - exportedNameDoc.internal = false; + // The exported doc should not be private. + doc.privateExport = false; + doc.internal = false; } }); } diff --git a/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.spec.js b/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.spec.js index 252e1bc483c..cde145e98e8 100644 --- a/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.spec.js +++ b/aio/tools/transforms/angular-api-package/processors/mergeOverriddenImplementation.spec.js @@ -39,15 +39,8 @@ describe('mergeOverriddenImplementation processor', () => { it('should replace properties with those from the implementation and constructor docs', () => { const exportedNameDoc = { - overriddenImplementation: 'Foo has an overridden implementation.', // This processor should apply - declaration: { // Imitate a valid AST - name: {getText: () => 'Foo'}, - type: {getText: () => 'FooCtor'}, - initializer: { - expression: {getText: () => 'FooImpl'}, - }, - }, - exportedProp: true, // This prop will be removed + overriddenImplementation: 'FooCtor', // This processor should apply + exportedProp: true, // Documentation on the exported interface will remain }; const docs = [exportedNameDoc]; @@ -55,7 +48,6 @@ describe('mergeOverriddenImplementation processor', () => { switch(docName) { case 'Foo': return [exportedNameDoc]; case 'FooCtor': return [{ctorProp: true, members: [{name: 'new'}]}]; - case 'FooImpl': return [{implProp: true}]; } }; @@ -63,8 +55,9 @@ describe('mergeOverriddenImplementation processor', () => { processor.$process(docs); expect(docs).toEqual([{ - // Property copied from the implementation - implProp: true, + overriddenImplementation: 'FooCtor', + // Original documentation property + exportedProp: true, // Constructor signature property constructorDoc: {name: 'new'}, // The exported symbol should be explicitly marked non-internal diff --git a/goldens/public-api/forms/forms.md b/goldens/public-api/forms/forms.md index 182b6c72e63..40bfc39c2b1 100644 --- a/goldens/public-api/forms/forms.md +++ b/goldens/public-api/forms/forms.md @@ -296,8 +296,7 @@ export class FormBuilder { } // @public -export class FormControl extends AbstractControl { - constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); +export interface FormControl extends AbstractControl { readonly defaultValue: any; patchValue(value: any, options?: { onlySelf?: boolean; @@ -319,6 +318,9 @@ export class FormControl extends AbstractControl { }): void; } +// @public (undocumented) +export const FormControl: ɵFormControlCtor; + // @public export class FormControlDirective extends NgControl implements OnChanges, OnDestroy { constructor(validators: (Validator | ValidatorFn)[], asyncValidators: (AsyncValidator | AsyncValidatorFn)[], valueAccessors: ControlValueAccessor[], _ngModelWarningConfig: string | null); diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 712f8c413f8..4eefc1b1c36 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -17,7 +17,6 @@ * explicitly. */ - export {ɵInternalFormsSharedModule} from './directives'; export {AbstractControlDirective} from './directives/abstract_control_directive'; export {AbstractFormGroupDirective} from './directives/abstract_form_group_directive'; @@ -43,7 +42,7 @@ export {NgSelectOption, SelectControlValueAccessor} from './directives/select_co export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor'; export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators'; export {FormBuilder} from './form_builder'; -export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormControlStatus, FormGroup} from './model'; +export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormControlStatus, FormGroup, ɵFormControlCtor} from './model'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; export {VERSION} from './version'; diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index 9d5b5ca78ba..db242a343ce 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -161,9 +161,9 @@ export interface AbstractControlOptions { export interface FormControlOptions extends AbstractControlOptions { /** * @description - * Whether to use the initial value used to construct the FormControl as its default value as - * well. If this option is false or not provided, the default value of a FormControl is `null`. - * When a FormControl is {@link FormControl#reset} without an explicit value, its value reverts to + * Whether to use the initial value used to construct the {@link FormControl} as its default value + * as well. If this option is false or not provided, the default value of a FormControl is `null`. + * When a FormControl is reset without an explicit value, its value reverts to * its default value. */ initialValueIsDefault?: boolean; @@ -1177,7 +1177,6 @@ export abstract class AbstractControl { this._updateOn = opts.updateOn!; } } - /** * Check to see if parent has been marked artificially dirty. * @@ -1201,6 +1200,10 @@ export abstract class AbstractControl { * @see [Reactive Forms Guide](guide/reactive-forms) * @see [Usage Notes](#usage-notes) * + * @publicApi + * + * @overriddenImplementation ɵFormControlCtor + * * @usageNotes * * ### Initializing Form Controls @@ -1210,7 +1213,7 @@ export abstract class AbstractControl { * ```ts * const control = new FormControl('some value'); * console.log(control.value); // 'some value' - *``` + * ``` * * The following example initializes the control with a form state object. The `value` * and `disabled` keys are required in this case. @@ -1283,20 +1286,17 @@ export abstract class AbstractControl { * console.log(control.value); // 'Drew' * console.log(control.status); // 'DISABLED' * ``` - * - * @publicApi */ -export class FormControl extends AbstractControl { +export interface FormControl extends AbstractControl { /** * The default value of this FormControl, used whenever the control is reset without an explicit * value. See {@link FormControlOptions#initialValueIsDefault} for more information on configuring * a default value. - * @publicApi */ - public readonly defaultValue: any = null; + readonly defaultValue: any; /** @internal */ - _onChange: Array = []; + _onChange: Function[]; /** * This field holds a pending value that has not yet been applied to the form's value. @@ -1306,44 +1306,7 @@ export class FormControl extends AbstractControl { _pendingValue: any; /** @internal */ - _pendingChange: boolean = false; - - /** - * Creates a new `FormControl` instance. - * - * @param formState Initializes the control with an initial value, - * or an object that defines the initial value and disabled state. - * - * @param validatorOrOpts A synchronous validator function, or an array of - * such functions, or an `AbstractControlOptions` object that contains validation functions - * and a validation trigger. - * - * @param asyncValidator A single async validator or array of async validator functions - * - */ - constructor( - formState: any = null, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { - super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); - this._applyFormState(formState); - this._setUpdateStrategy(validatorOrOpts); - this._initObservables(); - this.updateValueAndValidity({ - onlySelf: true, - // If `asyncValidator` is present, it will trigger control status change from `PENDING` to - // `VALID` or `INVALID`. - // The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent` - // to `true` to allow that during the control creation process. - emitEvent: !!this.asyncValidator - }); - if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) { - if (this._isBoxedValue(formState)) { - this.defaultValue = formState.value; - } else { - this.defaultValue = formState; - } - } - } + _pendingChange: boolean; /** * Sets a new value for the form control. @@ -1368,19 +1331,12 @@ export class FormControl extends AbstractControl { * event to update the model. * */ - override setValue(value: any, options: { + setValue(value: any, options?: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, emitViewToModelChange?: boolean - } = {}): void { - (this as {value: any}).value = this._pendingValue = value; - if (this._onChange.length && options.emitModelToViewChange !== false) { - this._onChange.forEach( - (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false)); - } - this.updateValueAndValidity(options); - } + }): void; /** * Patches the value of a control. @@ -1391,14 +1347,12 @@ export class FormControl extends AbstractControl { * * @see `setValue` for options */ - override patchValue(value: any, options: { + patchValue(value: any, options?: { onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean, emitViewToModelChange?: boolean - } = {}): void { - this.setValue(value, options); - } + }): void; /** * Resets the form control, marking it `pristine` and `untouched`, and resetting @@ -1433,99 +1387,229 @@ export class FormControl extends AbstractControl { * When false, no events are emitted. * */ - override reset(formState: any = this.defaultValue, options: { - onlySelf?: boolean, - emitEvent?: boolean - } = {}): void { - this._applyFormState(formState); - this.markAsPristine(options); - this.markAsUntouched(options); - this.setValue(this.value, options); - this._pendingChange = false; - } + reset(formState?: any, options?: {onlySelf?: boolean, emitEvent?: boolean}): void; /** * @internal */ - override _updateValue() {} + _updateValue(): void; /** * @internal */ - override _anyControls(condition: (c: AbstractControl) => boolean): boolean { - return false; - } + _anyControls(condition: (c: AbstractControl) => boolean): boolean; /** * @internal */ - override _allControlsDisabled(): boolean { - return this.disabled; - } + _allControlsDisabled(): boolean; + /** * Register a listener for change events. * * @param fn The method that is called when the value changes */ - registerOnChange(fn: Function): void { - this._onChange.push(fn); - } + registerOnChange(fn: Function): void; + /** * Internal function to unregister a change events listener. * @internal */ - _unregisterOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void { - removeListItem(this._onChange, fn); - } + _unregisterOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void; /** * Register a listener for disabled events. * * @param fn The method that is called when the disabled status changes. */ - registerOnDisabledChange(fn: (isDisabled: boolean) => void): void { - this._onDisabledChange.push(fn); - } + registerOnDisabledChange(fn: (isDisabled: boolean) => void): void; /** * Internal function to unregister a disabled event listener. * @internal */ - _unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void { - removeListItem(this._onDisabledChange, fn); - } + _unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void; /** * @internal */ - override _forEachChild(cb: (c: AbstractControl) => void): void {} + _forEachChild(cb: (c: AbstractControl) => void): void; /** @internal */ - override _syncPendingControls(): boolean { - if (this.updateOn === 'submit') { - if (this._pendingDirty) this.markAsDirty(); - if (this._pendingTouched) this.markAsTouched(); - if (this._pendingChange) { - this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false}); - return true; - } - } - return false; - } - - private _applyFormState(formState: any) { - if (this._isBoxedValue(formState)) { - (this as {value: any}).value = this._pendingValue = formState.value; - formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) : - this.enable({onlySelf: true, emitEvent: false}); - } else { - (this as {value: any}).value = this._pendingValue = formState; - } - } + _syncPendingControls(): boolean; } +type FormControlInterface = FormControl; + +/** + * Various available constructors for `FormControl`. + * Do not use this interface directly. Instead, use `FormControl`: + * ``` + * const fc = new FormControl('foo'); + * ``` + * This symbol is prefixed with ɵ to make plain that it is an internal symbol. + */ +export interface ɵFormControlCtor { + /** + * Construct a FormControl with no initial value or validators. + */ + new(): FormControl; + + /** + * Creates a new `FormControl` instance. + * + * @param formState Initializes the control with an initial value, + * or an object that defines the initial value and disabled state. + * + * @param validatorOrOpts A synchronous validator function, or an array of + * such functions, or a `FormControlOptions` object that contains validation functions + * and a validation trigger. + * + * @param asyncValidator A single async validator or array of async validator functions. + */ + new(formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl; + + /** + * The presence of an explicit `prototype` property provides backwards-compatibility for apps that + * manually inspect the prototype chain. + */ + prototype: FormControl; +} + +export const FormControl: ɵFormControlCtor = + (class FormControl extends AbstractControl implements FormControlInterface { + /** @publicApi */ + public readonly defaultValue: any = null; + + /** @internal */ + _onChange: Function[] = []; + + /** @internal */ + _pendingValue: any; + + /** @internal */ + _pendingChange: boolean = false; + + constructor( + formState: any = null, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); + this._applyFormState(formState); + this._setUpdateStrategy(validatorOrOpts); + this._initObservables(); + this.updateValueAndValidity({ + onlySelf: true, + // If `asyncValidator` is present, it will trigger control status change from `PENDING` to + // `VALID` or `INVALID`. + // The status should be broadcasted via the `statusChanges` observable, so we set + // `emitEvent` to `true` to allow that during the control creation process. + emitEvent: !!this.asyncValidator + }); + if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) { + if (this._isBoxedValue(formState)) { + (this.defaultValue as any) = formState.value; + } else { + (this.defaultValue as any) = formState; + } + } + } + + override setValue(value: any, options: { + onlySelf?: boolean, + emitEvent?: boolean, + emitModelToViewChange?: boolean, + emitViewToModelChange?: boolean + } = {}): void { + (this as {value: any}).value = this._pendingValue = value; + if (this._onChange.length && options.emitModelToViewChange !== false) { + this._onChange.forEach( + (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false)); + } + this.updateValueAndValidity(options); + } + + override patchValue(value: any, options: { + onlySelf?: boolean, + emitEvent?: boolean, + emitModelToViewChange?: boolean, + emitViewToModelChange?: boolean + } = {}): void { + this.setValue(value, options); + } + + override reset( + formState: any = this.defaultValue, + options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + this._applyFormState(formState); + this.markAsPristine(options); + this.markAsUntouched(options); + this.setValue(this.value, options); + this._pendingChange = false; + } + + /** @internal */ + override _updateValue(): void {} + + /** @internal */ + override _anyControls(condition: (c: AbstractControl) => boolean): boolean { + return false; + } + + /** @internal */ + override _allControlsDisabled(): boolean { + return this.disabled; + } + + registerOnChange(fn: Function): void { + this._onChange.push(fn); + } + + /** @internal */ + _unregisterOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void { + removeListItem(this._onChange, fn); + } + + registerOnDisabledChange(fn: (isDisabled: boolean) => void): void { + this._onDisabledChange.push(fn); + } + + /** @internal */ + _unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void { + removeListItem(this._onDisabledChange, fn); + } + + /** @internal */ + override _forEachChild(cb: (c: AbstractControl) => void): void {} + + /** @internal */ + override _syncPendingControls(): boolean { + if (this.updateOn === 'submit') { + if (this._pendingDirty) this.markAsDirty(); + if (this._pendingTouched) this.markAsTouched(); + if (this._pendingChange) { + this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false}); + return true; + } + } + return false; + } + + private _applyFormState(formState: any) { + if (this._isBoxedValue(formState)) { + (this as {value: any}).value = this._pendingValue = formState.value; + formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) : + this.enable({onlySelf: true, emitEvent: false}); + } else { + (this as {value: any}).value = this._pendingValue = formState; + } + } + }); + /** * Tracks the value and validity state of a group of `FormControl` instances. * diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 16efb9a0d60..4e86a61578e 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -1526,5 +1526,56 @@ describe('FormControl', () => { }); }); }); + + describe('can be extended', () => { + // We don't technically support extending Forms classes, but people do it anyway. + // We need to make sure that there is some way to extend them to avoid causing breakage. + + class FCExt extends FormControl { + constructor(formState?: any|{ + value?: any; + disabled?: boolean; + }, ...args: any) { + super(formState, ...args); + } + } + + it('should perform basic FormControl operations', () => { + const nc = new FCExt({value: 'foo'}); + nc.setValue('bar'); + // There is no need to assert because, if this test compiles, then it is possible to correctly + // extend FormControl. + }); + }); + + describe('inspecting the prototype still provides FormControl type', () => { + // The constructor should be a function with a prototype property of T. + // (This is the assumption we don't want to break.) + type Constructor = Function&{prototype: T}; + + function isInstanceOf(value: Constructor, arg: unknown): arg is T { + // The implementation does not matter; we want to check whether this guard narrows the type. + return true; + } + + // This is a nullable FormControl, and we want isInstanceOf to narrow the type. + const fcOrNull: FormControl|null = new FormControl(42); + + it('and is appropriately narrowed', () => { + if (isInstanceOf(FormControl, fcOrNull)) { + // If the guard does not work, then this code will not compile due to null being in the + // type. + fcOrNull.setValue(7); + } + }); + }); + + describe('Function.name', () => { + it('returns FormControl', () => { + // This is always true in the trivial case, but can be broken by various methods of overriding + // FormControl's exported constructor. + expect(FormControl.name).toBe('FormControl'); + }); + }); }); })();