Skip to content

Commit 28ac0c4

Browse files
authored
fix: improve types in FieldRegistry (#8062)
* fix: improve types in FieldRegistry * chore: tsdoc
1 parent 54eeb85 commit 28ac0c4

11 files changed

Lines changed: 95 additions & 24 deletions

core/field.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,22 @@ export abstract class Field<T = any>
14331433
workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw();
14341434
}
14351435
}
1436+
1437+
/**
1438+
* Subclasses should reimplement this method to construct their Field
1439+
* subclass from a JSON arg object.
1440+
*
1441+
* It is an error to attempt to register a field subclass in the
1442+
* FieldRegistry if that subclass has not overridden this method.
1443+
*
1444+
* @param _options JSON configuration object with properties needed
1445+
* to configure a specific field.
1446+
*/
1447+
static fromJson(_options: FieldConfig): Field {
1448+
throw new Error(
1449+
`Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`,
1450+
);
1451+
}
14361452
}
14371453

14381454
/**
@@ -1443,12 +1459,14 @@ export interface FieldConfig {
14431459
}
14441460

14451461
/**
1446-
* For use by Field and descendants of Field. Constructors can change
1462+
* Represents an object that has all the prototype properties of the `Field`
1463+
* class. This is necessary because constructors can change
14471464
* in descendants, though they should contain all of Field's prototype methods.
14481465
*
1449-
* @internal
1466+
* This type should only be used in places where we directly access the prototype
1467+
* of a Field class or subclass.
14501468
*/
1451-
export type FieldProto = Pick<typeof Field, 'prototype'>;
1469+
type FieldProto = Pick<typeof Field, 'prototype'>;
14521470

14531471
/**
14541472
* Represents an error where the field is trying to access its block or

core/field_angle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ export class FieldAngle extends FieldInput<number> {
516516
* @nocollapse
517517
* @internal
518518
*/
519-
static fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
519+
static override fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
520520
// `this` might be a subclass of FieldAngle if that class doesn't override
521521
// the static fromJson method.
522522
return new this(options.angle, undefined, options);

core/field_checkbox.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@ export class FieldCheckbox extends Field<CheckboxBool> {
226226
* @nocollapse
227227
* @internal
228228
*/
229-
static fromJson(options: FieldCheckboxFromJsonConfig): FieldCheckbox {
229+
static override fromJson(
230+
options: FieldCheckboxFromJsonConfig,
231+
): FieldCheckbox {
230232
// `this` might be a subclass of FieldCheckbox if that class doesn't
231233
// 'override' the static fromJson method.
232234
return new this(options.checked, undefined, options);

core/field_colour.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ export class FieldColour extends Field<string> {
641641
* @nocollapse
642642
* @internal
643643
*/
644-
static fromJson(options: FieldColourFromJsonConfig): FieldColour {
644+
static override fromJson(options: FieldColourFromJsonConfig): FieldColour {
645645
// `this` might be a subclass of FieldColour if that class doesn't override
646646
// the static fromJson method.
647647
return new this(options.colour, undefined, options);

core/field_dropdown.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,9 @@ export class FieldDropdown extends Field<string> {
647647
* @nocollapse
648648
* @internal
649649
*/
650-
static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown {
650+
static override fromJson(
651+
options: FieldDropdownFromJsonConfig,
652+
): FieldDropdown {
651653
if (!options.options) {
652654
throw new Error(
653655
'options are required for the dropdown field. The ' +

core/field_image.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export class FieldImage extends Field<string> {
250250
* @nocollapse
251251
* @internal
252252
*/
253-
static fromJson(options: FieldImageFromJsonConfig): FieldImage {
253+
static override fromJson(options: FieldImageFromJsonConfig): FieldImage {
254254
if (!options.src || !options.width || !options.height) {
255255
throw new Error(
256256
'src, width, and height values for an image field are' +

core/field_label.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class FieldLabel extends Field<string> {
117117
* @nocollapse
118118
* @internal
119119
*/
120-
static fromJson(options: FieldLabelFromJsonConfig): FieldLabel {
120+
static override fromJson(options: FieldLabelFromJsonConfig): FieldLabel {
121121
const text = parsing.replaceMessageReferences(options.text);
122122
// `this` might be a subclass of FieldLabel if that class doesn't override
123123
// the static fromJson method.

core/field_number.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export class FieldNumber extends FieldInput<number> {
315315
* @nocollapse
316316
* @internal
317317
*/
318-
static fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
318+
static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
319319
// `this` might be a subclass of FieldNumber if that class doesn't override
320320
// the static fromJson method.
321321
return new this(

core/field_registry.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,46 @@
66

77
// Former goog.module ID: Blockly.fieldRegistry
88

9-
import type {Field, FieldProto} from './field.js';
9+
import type {Field, FieldConfig} from './field.js';
1010
import * as registry from './registry.js';
1111

12-
interface RegistryOptions {
12+
/**
13+
* When constructing a field from JSON using the registry, the
14+
* `fromJson` method in this file is called with an options parameter
15+
* object consisting of the "type" which is the name of the field, and
16+
* other options that are part of the field's config object.
17+
*
18+
* These options are then passed to the field's static `fromJson`
19+
* method. That method accepts an options parameter with a type that usually
20+
* extends from FieldConfig, and may or may not have a "type" attribute (in
21+
* fact, it shouldn't, because we'd overwrite it as described above!)
22+
*
23+
* Unfortunately the registry has no way of knowing the actual Field subclass
24+
* that will be returned from passing in the name of the field. Therefore it
25+
* also has no way of knowing that the options object not only implements
26+
* `FieldConfig`, but it also should satisfy the Config that belongs to that
27+
* specific class's `fromJson` method.
28+
*
29+
* Because of this uncertainty, we just give up on type checking the properties
30+
* passed to the `fromJson` method, and allow arbitrary string keys with
31+
* unknown types.
32+
*/
33+
type RegistryOptions = FieldConfig & {
34+
// The name of the field, e.g. field_dropdown
1335
type: string;
1436
[key: string]: unknown;
37+
};
38+
39+
/**
40+
* Represents the static methods that must be defined on any
41+
* field that is registered, i.e. the constructor and fromJson methods.
42+
*
43+
* Because we don't know which Field subclass will be registered, we
44+
* are unable to typecheck the parameters of the constructor.
45+
*/
46+
export interface RegistrableField {
47+
new (...args: any[]): Field;
48+
fromJson(options: FieldConfig): Field;
1549
}
1650

1751
/**
@@ -25,7 +59,7 @@ interface RegistryOptions {
2559
* @throws {Error} if the type name is empty, the field is already registered,
2660
* or the fieldClass is not an object containing a fromJson function.
2761
*/
28-
export function register(type: string, fieldClass: FieldProto) {
62+
export function register(type: string, fieldClass: RegistrableField) {
2963
registry.register(registry.Type.FIELD, type, fieldClass);
3064
}
3165

@@ -59,7 +93,10 @@ export function fromJson<T>(options: RegistryOptions): Field<T> | null {
5993
* @param options
6094
*/
6195
function fromJsonInternal<T>(options: RegistryOptions): Field<T> | null {
62-
const fieldObject = registry.getObject(registry.Type.FIELD, options.type);
96+
const fieldObject = registry.getObject(
97+
registry.Type.FIELD,
98+
options.type,
99+
) as unknown as RegistrableField;
63100
if (!fieldObject) {
64101
console.warn(
65102
'Blockly could not create a field of type ' +
@@ -69,12 +106,8 @@ function fromJsonInternal<T>(options: RegistryOptions): Field<T> | null {
69106
' #1584), or the registration is not being reached.',
70107
);
71108
return null;
72-
} else if (typeof (fieldObject as any).fromJson !== 'function') {
73-
throw new TypeError('returned Field was not a IRegistrableField');
74-
} else {
75-
type fromJson = (options: {}) => Field<T>;
76-
return (fieldObject as unknown as {fromJson: fromJson}).fromJson(options);
77109
}
110+
return fieldObject.fromJson(options);
78111
}
79112

80113
export const TEST_ONLY = {

core/field_textinput.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export class FieldTextInput extends FieldInput<string> {
7373
* @nocollapse
7474
* @internal
7575
*/
76-
static fromJson(options: FieldTextInputFromJsonConfig): FieldTextInput {
76+
static override fromJson(
77+
options: FieldTextInputFromJsonConfig,
78+
): FieldTextInput {
7779
const text = parsing.replaceMessageReferences(options.text);
7880
// `this` might be a subclass of FieldTextInput if that class doesn't
7981
// override the static fromJson method.

0 commit comments

Comments
 (0)