Skip to content

Commit 966f0f7

Browse files
Interactivity: Strict type checking (#59865)
Enable `strictNullChecks` and improve types. Co-authored-by: sirreal <[email protected]> Co-authored-by: cbravobernal <[email protected]>
1 parent 7a9f1de commit 966f0f7

File tree

8 files changed

+141
-85
lines changed

8 files changed

+141
-85
lines changed

packages/interactivity/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
## Unreleased
44

5+
### Enhancements
6+
7+
- Strict type checking: fix some missing nulls in published types ([#59865](https://github.com/WordPress/gutenberg/pull/59865/)).
8+
59
### Bug Fixes
610

711
- Allow multiple event handlers for the same type with `data-wp-on-document` and `data-wp-on-window`. ([#61009](https://github.com/WordPress/gutenberg/pull/61009))
8-
912
- Prevent wrong written directives from killing the runtime ([#61249](https://github.com/WordPress/gutenberg/pull/61249))
1013
- Prevent empty namespace or different namespaces from killing the runtime ([#61409](https://github.com/WordPress/gutenberg/pull/61409))
1114

packages/interactivity/src/directives.js renamed to packages/interactivity/src/directives.tsx

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
// eslint-disable-next-line eslint-comments/disable-enable-pair
2+
/* eslint-disable react-hooks/exhaustive-deps */
3+
14
/* @jsx createElement */
25

36
/**
47
* External dependencies
58
*/
6-
import { h as createElement } from 'preact';
9+
import { h as createElement, type RefObject } from 'preact';
710
import { useContext, useMemo, useRef } from 'preact/hooks';
8-
import { deepSignal, peek } from 'deepsignal';
11+
import { deepSignal, peek, type DeepSignal } from 'deepsignal';
912

1013
/**
1114
* Internal dependencies
@@ -23,8 +26,8 @@ const contextObjectToProxy = new WeakMap();
2326
const contextProxyToObject = new WeakMap();
2427
const contextObjectToFallback = new WeakMap();
2528

26-
const isPlainObject = ( item ) =>
27-
item && typeof item === 'object' && item.constructor === Object;
29+
const isPlainObject = ( item: unknown ): boolean =>
30+
Boolean( item && typeof item === 'object' && item.constructor === Object );
2831

2932
const descriptor = Reflect.getOwnPropertyDescriptor;
3033

@@ -37,17 +40,17 @@ const descriptor = Reflect.getOwnPropertyDescriptor;
3740
* By default, all plain objects inside the context are wrapped, unless it is
3841
* listed in the `ignore` option.
3942
*
40-
* @param {Object} current Current context.
41-
* @param {Object} inherited Inherited context, used as fallback.
43+
* @param current Current context.
44+
* @param inherited Inherited context, used as fallback.
4245
*
43-
* @return {Object} The wrapped context object.
46+
* @return The wrapped context object.
4447
*/
45-
const proxifyContext = ( current, inherited = {} ) => {
48+
const proxifyContext = ( current: object, inherited: object = {} ): object => {
4649
// Update the fallback object reference when it changes.
4750
contextObjectToFallback.set( current, inherited );
4851
if ( ! contextObjectToProxy.has( current ) ) {
4952
const proxy = new Proxy( current, {
50-
get: ( target, k ) => {
53+
get: ( target: DeepSignal< any >, k ) => {
5154
const fallback = contextObjectToFallback.get( current );
5255
// Always subscribe to prop changes in the current context.
5356
const currentProp = target[ k ];
@@ -127,10 +130,13 @@ const proxifyContext = ( current, inherited = {} ) => {
127130
/**
128131
* Recursively update values within a deepSignal object.
129132
*
130-
* @param {Object} target A deepSignal instance.
131-
* @param {Object} source Object with properties to update in `target`
133+
* @param target A deepSignal instance.
134+
* @param source Object with properties to update in `target`.
132135
*/
133-
const updateSignals = ( target, source ) => {
136+
const updateSignals = (
137+
target: DeepSignal< any >,
138+
source: DeepSignal< any >
139+
) => {
134140
for ( const k in source ) {
135141
if (
136142
isPlainObject( peek( target, k ) ) &&
@@ -146,23 +152,23 @@ const updateSignals = ( target, source ) => {
146152
/**
147153
* Recursively clone the passed object.
148154
*
149-
* @param {Object} source Source object.
150-
* @return {Object} Cloned object.
155+
* @param source Source object.
156+
* @return Cloned object.
151157
*/
152-
const deepClone = ( source ) => {
158+
function deepClone< T >( source: T ): T {
153159
if ( isPlainObject( source ) ) {
154160
return Object.fromEntries(
155-
Object.entries( source ).map( ( [ key, value ] ) => [
161+
Object.entries( source as object ).map( ( [ key, value ] ) => [
156162
key,
157163
deepClone( value ),
158164
] )
159-
);
165+
) as T;
160166
}
161167
if ( Array.isArray( source ) ) {
162-
return source.map( ( i ) => deepClone( i ) );
168+
return source.map( ( i ) => deepClone( i ) ) as T;
163169
}
164170
return source;
165-
};
171+
}
166172

167173
const newRule =
168174
/(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
@@ -176,10 +182,12 @@ const empty = ' ';
176182
* Made by Cristian Bote (@cristianbote) for Goober.
177183
* https://unpkg.com/browse/[email protected]/src/core/astish.js
178184
*
179-
* @param {string} val CSS string.
180-
* @return {Object} CSS object.
185+
* @param val CSS string.
186+
* @return CSS object.
181187
*/
182-
const cssStringToObject = ( val ) => {
188+
const cssStringToObject = (
189+
val: string
190+
): Record< string, string | number > => {
183191
const tree = [ {} ];
184192
let block, left;
185193

@@ -203,10 +211,9 @@ const cssStringToObject = ( val ) => {
203211
* Creates a directive that adds an event listener to the global window or
204212
* document object.
205213
*
206-
* @param {string} type 'window' or 'document'
207-
* @return {void}
214+
* @param type 'window' or 'document'
208215
*/
209-
const getGlobalEventDirective = ( type ) => {
216+
const getGlobalEventDirective = ( type: 'window' | 'document' ) => {
210217
return ( { directives, evaluate } ) => {
211218
directives[ `on-${ type }` ]
212219
.filter( ( { suffix } ) => suffix !== 'default' )
@@ -217,7 +224,7 @@ const getGlobalEventDirective = ( type ) => {
217224
const globalVar = type === 'window' ? window : document;
218225
globalVar.addEventListener( eventName, cb );
219226
return () => globalVar.removeEventListener( eventName, cb );
220-
}, [] );
227+
} );
221228
} );
222229
};
223230
};
@@ -333,9 +340,13 @@ export default () => {
333340
* need deps because it only needs to do it the first time.
334341
*/
335342
if ( ! result ) {
336-
element.ref.current.classList.remove( className );
343+
(
344+
element.ref as RefObject< HTMLElement >
345+
).current!.classList.remove( className );
337346
} else {
338-
element.ref.current.classList.add( className );
347+
(
348+
element.ref as RefObject< HTMLElement >
349+
).current!.classList.add( className );
339350
}
340351
} );
341352
} );
@@ -368,9 +379,13 @@ export default () => {
368379
* because it only needs to do it the first time.
369380
*/
370381
if ( ! result ) {
371-
element.ref.current.style.removeProperty( styleProp );
382+
(
383+
element.ref as RefObject< HTMLElement >
384+
).current!.style.removeProperty( styleProp );
372385
} else {
373-
element.ref.current.style[ styleProp ] = result;
386+
(
387+
element.ref as RefObject< HTMLElement >
388+
).current!.style[ styleProp ] = result;
374389
}
375390
} );
376391
} );
@@ -390,7 +405,8 @@ export default () => {
390405
* first time. After that, Preact will handle the changes.
391406
*/
392407
useInit( () => {
393-
const el = element.ref.current;
408+
const el = ( element.ref as RefObject< HTMLElement > )
409+
.current!;
394410

395411
/*
396412
* We set the value directly to the corresponding HTMLElement instance
@@ -462,6 +478,8 @@ export default () => {
462478
type: Type,
463479
props: { innerHTML, ...rest },
464480
},
481+
}: {
482+
element: any;
465483
} ) => {
466484
// Preserve the initial inner HTML.
467485
const cached = useMemo( () => innerHTML, [] );
@@ -477,6 +495,11 @@ export default () => {
477495
// data-wp-text
478496
directive( 'text', ( { directives: { text }, element, evaluate } ) => {
479497
const entry = text.find( ( { suffix } ) => suffix === 'default' );
498+
if ( ! entry ) {
499+
element.props.children = null;
500+
return;
501+
}
502+
480503
try {
481504
const result = evaluate( entry );
482505
element.props.children =

packages/interactivity/src/hooks.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
/* @jsx createElement */
22

3+
// eslint-disable-next-line eslint-comments/disable-enable-pair
4+
/* eslint-disable react-hooks/exhaustive-deps */
5+
36
/**
47
* External dependencies
58
*/
@@ -8,6 +11,7 @@ import {
811
options,
912
createContext,
1013
cloneElement,
14+
type ComponentChildren,
1115
} from 'preact';
1216
import { useRef, useCallback, useContext } from 'preact/hooks';
1317
import type { VNode, Context, RefObject } from 'preact';
@@ -18,7 +22,7 @@ import type { VNode, Context, RefObject } from 'preact';
1822
import { store, stores, universalUnlock } from './store';
1923
import { warn } from './utils/warn';
2024
interface DirectiveEntry {
21-
value: string | Object;
25+
value: string | object;
2226
namespace: string;
2327
suffix: string;
2428
}
@@ -33,11 +37,15 @@ interface DirectiveArgs {
3337
/**
3438
* Props present in the current element.
3539
*/
36-
props: Object;
40+
props: { children?: ComponentChildren };
3741
/**
3842
* Virtual node representing the element.
3943
*/
40-
element: VNode;
44+
element: VNode< {
45+
class?: string;
46+
style?: string | Record< string, string | number >;
47+
content?: ComponentChildren;
48+
} >;
4149
/**
4250
* The inherited context.
4351
*/
@@ -50,7 +58,7 @@ interface DirectiveArgs {
5058
}
5159

5260
interface DirectiveCallback {
53-
( args: DirectiveArgs ): VNode | void;
61+
( args: DirectiveArgs ): VNode | null | void;
5462
}
5563

5664
interface DirectiveOptions {
@@ -65,7 +73,7 @@ interface DirectiveOptions {
6573

6674
interface Scope {
6775
evaluate: Evaluate;
68-
context: Context< any >;
76+
context: object;
6977
ref: RefObject< HTMLElement >;
7078
attributes: createElement.JSX.HTMLAttributes;
7179
}
@@ -102,7 +110,7 @@ const immutableError = () => {
102110
'Please use `data-wp-bind` to modify the attributes of an element.'
103111
);
104112
};
105-
const immutableHandlers = {
113+
const immutableHandlers: ProxyHandler< object > = {
106114
get( target, key, receiver ) {
107115
const value = Reflect.get( target, key, receiver );
108116
return !! value && typeof value === 'object'
@@ -112,7 +120,7 @@ const immutableHandlers = {
112120
set: immutableError,
113121
deleteProperty: immutableError,
114122
};
115-
const deepImmutable = < T extends Object = {} >( target: T ): T => {
123+
const deepImmutable = < T extends object = {} >( target: T ): T => {
116124
if ( ! immutableMap.has( target ) ) {
117125
immutableMap.set( target, new Proxy( target, immutableHandlers ) );
118126
}
@@ -260,7 +268,7 @@ export const directive = (
260268
};
261269

262270
// Resolve the path to some property of the store object.
263-
const resolve = ( path, namespace ) => {
271+
const resolve = ( path: string, namespace: string ) => {
264272
if ( ! namespace ) {
265273
warn(
266274
`The "namespace" cannot be "{}", "null" or an empty string. Path: ${ path }`

packages/interactivity/src/store.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from './hooks';
1818

1919
const isObject = ( item: unknown ): item is Record< string, unknown > =>
20-
item && typeof item === 'object' && item.constructor === Object;
20+
Boolean( item && typeof item === 'object' && item.constructor === Object );
2121

2222
const deepMerge = ( target: any, source: any ) => {
2323
if ( isObject( target ) && isObject( source ) ) {
@@ -338,12 +338,12 @@ export const populateInitialData = ( data?: {
338338
config?: Record< string, unknown >;
339339
} ) => {
340340
if ( isObject( data?.state ) ) {
341-
Object.entries( data.state ).forEach( ( [ namespace, state ] ) => {
341+
Object.entries( data!.state ).forEach( ( [ namespace, state ] ) => {
342342
store( namespace, { state }, { lock: universalUnlock } );
343343
} );
344344
}
345345
if ( isObject( data?.config ) ) {
346-
Object.entries( data.config ).forEach( ( [ namespace, config ] ) => {
346+
Object.entries( data!.config ).forEach( ( [ namespace, config ] ) => {
347347
storeConfigs.set( namespace, config );
348348
} );
349349
}

packages/interactivity/src/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ const afterNextFrame = ( callback: () => void ) => {
6262
* @return The Flusher object with `flush` and `dispose` properties.
6363
*/
6464
function createFlusher( compute: () => unknown, notify: () => void ): Flusher {
65-
let flush: () => void;
66-
const dispose = effect( function () {
65+
let flush: () => void = () => undefined;
66+
const dispose = effect( function ( this: any ) {
6767
flush = this.c.bind( this );
6868
this.x = compute;
6969
this.c = notify;
@@ -82,7 +82,7 @@ function createFlusher( compute: () => unknown, notify: () => void ): Flusher {
8282
*/
8383
export function useSignalEffect( callback: () => unknown ) {
8484
_useEffect( () => {
85-
let eff = null;
85+
let eff: Flusher | null = null;
8686
let isExecuting = false;
8787

8888
const notify = async () => {
@@ -273,7 +273,7 @@ export const createRootFragment = (
273273
parent: Element,
274274
replaceNode: Node | Node[]
275275
) => {
276-
replaceNode = [].concat( replaceNode );
276+
replaceNode = ( [] as Node[] ).concat( replaceNode );
277277
const sibling = replaceNode[ replaceNode.length - 1 ].nextSibling;
278278
function insert( child: any, root: any ) {
279279
parent.insertBefore( child, root || sibling );

0 commit comments

Comments
 (0)