Skip to content

Commit

Permalink
fix(types): typing intensifies
Browse files Browse the repository at this point in the history
Type changes only, no real code changes.

- inside functions, handlers are QRLs only
- handler type inferral works through $()
- PropsOf now works on strings and inline components too, e.g. `PropsOf<'div'>`
- also type SVG elements and fix the incorrect SVG examples
- provide JSX.ElementType (supported since TS 5.1)
- examples now use the element argument of onInput$
- some more Qwik events are documented
- correct popover types from TS
- make input type smarter wrt. bind:value vs bind:checked
- fix api.ts: don't remove `{};` from api docs
  • Loading branch information
wmertens committed Dec 11, 2023
1 parent 505171c commit b59327e
Show file tree
Hide file tree
Showing 37 changed files with 751 additions and 738 deletions.
4 changes: 2 additions & 2 deletions packages/docs/src/routes/(ecosystem)/ecosystem/menu-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ export const MenuItems = () => {
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<g fill="none" stroke="currentColor" strokeLinejoin="round" strokeWidth="4">
<g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4">
<path d="M8 7h32v24H8z"></path>
<path
strokeLinecap="round"
stroke-linecap="round"
d="M4 7h40M15 41l9-10l9 10M16 13h16m-16 6h12m-12 6h6"
></path>
</g>
Expand Down
8 changes: 4 additions & 4 deletions packages/docs/src/routes/demo/resumability/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,14 @@ export function ReadyIcon(props: QwikIntrinsicElements['svg'], key: string) {
<g
fill="green"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M24 4v8"></path>
<path
d="m22 22l20 4l-6 4l6 6l-6 6l-6-6l-4 6l-4-20Z"
clipRule="evenodd"
clip-rule="evenodd"
></path>
<path d="m38.142 9.858l-5.657 5.657M9.858 38.142l5.657-5.657M4 24h8M9.858 9.858l5.657 5.657"></path>
</g>
Expand Down
6 changes: 1 addition & 5 deletions packages/docs/src/routes/demo/state/resource-agify/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ export default component$(() => {
<div>
<label>
Enter your name, and I'll guess your age!
<input
onInput$={(e: Event) =>
(name.value = (e.target as HTMLInputElement).value)
}
/>
<input onInput$={(ev, el) => (name.value = el.value)} />
</label>
</div>
<Resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,7 @@ export default component$(() => {
<label>
Enter your name, and I'll guess your age!
<input
onInput$={(e: Event) =>
(name.value = (e.target as HTMLInputElement).value)
}
onInput$={(ev, el) => (name.value = el.value)}
/>
</label>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export const MyComponent = component$(() => {
const count = useSignal(0);

// Notice the `$(...)` around the event handler function.
const inputHandler = $((event) => {
console.log('input', event.target.name, event.target.value);
const inputHandler = $((event, elem) => {
console.log(event.type, elem.value);
});

return (
Expand Down Expand Up @@ -224,8 +224,8 @@ export function exportedFunction() {

export default component$(() => {
// The first argument is a function.
const valid1 = $((event) => {
console.log('input', event.target.name, event.target.value);
const valid1 = $((event, elem) => {
console.log(event.type, elem.value);
});

// The first argument is an imported identifier.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@ export const AutoComplete = component$(() => {

return (
<div>
<input
type="text"
onInput$={(ev) => (state.searchInput = (ev.target as HTMLInputElement).value)}
/>
<input type="text" onInput$={(ev, el) => (state.searchInput = el.value)} />
<SuggestionsListComponent state={state}></SuggestionsListComponent>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export default component$(() => {
});
return (
<>
<input
value={store.value}
onInput$={(event) => (store.value = (event.target as HTMLInputElement).value)}
/>
<input value={store.value} onInput$={(ev, el) => (store.value = el.value)} />
<br />
Current value: {store.value}
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export default component$(() => {
});
return (
<>
<input
value={store.value}
onInput$={(event) => (store.value = (event.target as HTMLInputElement).value)}
/>
<input value={store.value} onInput$={(ev, el) => (store.value = el.value)} />
<br />
Current value: {store.value}
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ export default component$(() => {
<p>
<label>
GitHub username:
<input
value={github.org}
onInput$={(ev) => (github.org = (ev.target as HTMLInputElement).value)}
/>
<input value={github.org} onInput$={(ev, el) => (github.org = el.value)} />
</label>
</p>
<section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ export default component$(() => {
<p>
<label>
GitHub username:
<input
value={github.org}
onInput$={(ev) => (github.org = (ev.target as HTMLInputElement).value)}
/>
<input value={github.org} onInput$={(ev, el) => (github.org = el.value)} />
</label>
</p>
<section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export default component$(() => {
<p>
<label>
GitHub username:
<input
value={github.org}
onInput$={(ev) => (github.org = (ev.target as HTMLInputElement).value)}
/>
<input value={github.org} onInput$={(ev, el) => (github.org = el.value)} />
</label>
</p>
<section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export default component$(() => {
<label>
Enter your name followed by the enter key:{' '}
<input
// @ts-expect-error we can't infer through the $. Remove this line when solving
onInput$={$(async (event) => {
const input = event.target as HTMLInputElement;
onInput$={$(async (ev, input) => {
store.name = input.value;
})}
onChange$={$(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export default component$(() => {
<label>
Enter your name followed by the enter key:{' '}
<input
onInput$={(event) => {
const input = event.target as HTMLInputElement;
onInput$={(event, input) => {
store.name = input.value;
}}
onChange$={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ export default component$(() => {
<p>
<label>
GitHub username:
<input
value={github.org}
onInput$={(ev) => (github.org = (ev.target as HTMLInputElement).value)}
/>
<input value={github.org} onInput$={(ev, el) => (github.org = el.value)} />
</label>
</p>
<section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export default component$(() => {
<p>
<label>
GitHub username:
<input
value={github.org}
onInput$={(ev) => (github.org = (ev.target as HTMLInputElement).value)}
/>
<input value={github.org} onInput$={(ev, el) => el.value} />
</label>
</p>
<section>
Expand Down
39 changes: 27 additions & 12 deletions packages/qwik/src/core/component/component.public.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { $, type PropFnInterface, type QRL } from '../qrl/qrl.public';
import type { JSXNode } from '../render/jsx/types/jsx-node';
import { OnRenderProp, QSlot } from '../util/markers';
import type { ComponentBaseProps, JSXChildren } from '../render/jsx/types/jsx-qwik-attributes';
import type {
ComponentBaseProps,
EventHandler,
JSXChildren,
QRLEventHandlerMulti,
} from '../render/jsx/types/jsx-qwik-attributes';
import type { FunctionComponent } from '../render/jsx/types/jsx-node';
import { Virtual, _jsxC } from '../render/jsx/jsx-runtime';
import { SERIALIZABLE_STATE } from '../container/serializers';
Expand All @@ -10,6 +15,7 @@ import { assertQrl } from '../qrl/qrl-class';
import type { ValueOrPromise } from '../util/types';
import { _IMMUTABLE } from '../state/constants';
import { assertNumber } from '../error/assert';
import type { QwikIntrinsicElements } from '../render/jsx/types/jsx-qwik-elements';

/**
* Infers `Props` from the component.
Expand All @@ -23,9 +29,13 @@ import { assertNumber } from '../error/assert';
* @public
*/
// </docs>
export type PropsOf<COMP extends Component<any>> = COMP extends Component<infer PROPS>
export type PropsOf<COMP> = COMP extends Component<infer PROPS>
? NonNullable<PROPS>
: never;
: COMP extends FunctionComponent<infer PROPS>
? NonNullable<PublicProps<PROPS>>
: COMP extends string
? QwikIntrinsicElements[COMP]
: Record<string, unknown>;

/**
* Type representing the Qwik component.
Expand All @@ -43,7 +53,9 @@ export type PropsOf<COMP extends Component<any>> = COMP extends Component<infer
*
* @public
*/
export type Component<PROPS extends Record<any, any>> = FunctionComponent<PublicProps<PROPS>>;
export type Component<PROPS extends Record<any, any> = Record<string, unknown>> = FunctionComponent<
PublicProps<PROPS>
>;

export type ComponentChildren<PROPS extends Record<any, any>> = PROPS extends {
children: any;
Expand All @@ -65,16 +77,19 @@ export type PublicProps<PROPS extends Record<any, any>> = TransformProps<PROPS>
* @public
*/
export type TransformProps<PROPS extends Record<any, any>> = {
[K in keyof PROPS]: TransformProp<PROPS[K]>;
[K in keyof PROPS]: TransformProp<PROPS[K], K>;
};

/** @public */
export type TransformProp<T> = NonNullable<T> extends (...args: infer ARGS) => infer RET
export type TransformProp<T, K> = NonNullable<T> extends (...args: infer ARGS) => infer RET
? (...args: ARGS) => ValueOrPromise<Awaited<RET>>
: T;

/** @public */
export type EventHandler<T> = QRL<(value: T) => any>;
: T extends QRLEventHandlerMulti<infer EV, infer EL>
? EventHandler<EV, EL> | T
: K extends `${string}$`
? T extends QRL<infer U>
? T | U
: T
: T;

// const ELEMENTS_SKIP_KEY: JSXTagName[] = ['html', 'body', 'head'];

Expand Down Expand Up @@ -158,8 +173,8 @@ export const componentQrl = <PROPS extends Record<any, any>>(
return QwikComponent as any;
};

export const isQwikComponent = (component: any): component is Component<any> => {
return typeof component == 'function' && component[SERIALIZABLE_STATE] !== undefined;
export const isQwikComponent = <T extends Component<any>>(component: unknown): component is T => {
return typeof component == 'function' && (component as any)[SERIALIZABLE_STATE] !== undefined;
};

/** @public */
Expand Down
26 changes: 10 additions & 16 deletions packages/qwik/src/core/component/component.unit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,28 +116,19 @@ describe('q-component', () => {
);
});

test('types work as expected', () => {
test('types work as expected', () => () => {
// Let's keep one of these old type exports around for now.
const Input1 = component$<InputHTMLAttributes<HTMLInputElement>>((props) => {
return <input {...props} />;
});

const Input2 = component$((props: PropFunctionProps<InputHTMLAttributes<HTMLInputElement>>) => {
const Input2 = component$((props: PropFunctionProps<PropsOf<'input'>>) => {
return <input {...props} />;
});

const Input3 = component$((props: Partial<InputHTMLAttributes<HTMLInputElement>>) => {
return <input {...props} />;
});

const Input4 = component$(
(props: Partial<PropFunctionProps<InputHTMLAttributes<HTMLInputElement>>>) => {
return <input {...props} />;
}
);

type Input5Props = {
type: 'text' | 'number';
} & Partial<InputHTMLAttributes<HTMLInputElement>>;
} & Partial<PropsOf<'input'>>;

const Input5 = component$<Input5Props>(({ type, ...props }) => {
return <input type={type} {...props} />;
Expand All @@ -158,10 +149,13 @@ describe('q-component', () => {
component$(() => {
return (
<>
<Input1 value="1" />
<Input1
style={{
paddingInlineEnd: '10px',
}}
value="1"
/>
<Input2 value="2" />
<Input3 value="3" />
<Input4 value="4" />
<Input5 value="5" type="text" />
<Input6 value="6" type="number" />
</>
Expand Down
4 changes: 4 additions & 0 deletions packages/qwik/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type {
JSXChildren,
ComponentBaseProps,
ClassList,
CorrectedToggleEvent,
} from './render/jsx/types/jsx-qwik-attributes';
export type { FunctionComponent, JSXNode, DevJSX } from './render/jsx/types/jsx-node';
export type { QwikDOMAttributes, QwikJSX } from './render/jsx/types/jsx-qwik';
Expand Down Expand Up @@ -123,8 +124,11 @@ export { version } from './version';
// Qwik Events
//////////////////////////////////////////////////////////////////////////////////////////
export type {
KnownEventNames as KnownEventNames,
QwikSymbolEvent,
QwikVisibleEvent,
QwikIdleEvent,
QwikInitEvent,
// old
NativeAnimationEvent,
NativeClipboardEvent,
Expand Down
1 change: 1 addition & 0 deletions packages/qwik/src/core/qrl/qrl.public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export type PropFunction<T extends Function = (...args: any) => any> = T extends
*
* import { createContextId, useContext, useContextProvider } from './use/use-context';
* import { Resource, useResource$ } from './use/use-resource';
* import { useSignal } from './use/use-signal';
*
* export const greet = () => console.log('greet');
* function topLevelFn() {}
Expand Down
20 changes: 19 additions & 1 deletion packages/qwik/src/core/qrl/qrl.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { parseQRL, serializeQRL } from './qrl';
import { createQRL } from './qrl-class';
import { qrl } from './qrl';
import { describe, test, assert, assertType, expectTypeOf } from 'vitest';
import type { QRL } from './qrl.public';
import { $, type QRL } from './qrl.public';

function matchProps(obj: any, properties: Record<string, any>) {
for (const [key, value] of Object.entries(properties)) {
Expand All @@ -21,6 +21,24 @@ describe('types', () => {
expectTypeOf(fakeFn).not.toBeAny();
assertType<(hi: boolean) => Promise<string>>(fakeFn);
});
test('inferring', () => () => {
const myWrapper = (fn: QRL<(hi: boolean) => string>) => fn(true);
const result = myWrapper(
$((hi) => {
expectTypeOf(hi).toEqualTypeOf<boolean>();
return 'hello';
})
);
expectTypeOf(result).toEqualTypeOf<Promise<string>>();
const myPropsWrapper = (props: { fn: QRL<(hi: boolean) => string> }) => props.fn(true);
const propsResult = myPropsWrapper({
fn: $((hi) => {
expectTypeOf(hi).toEqualTypeOf<boolean>();
return 'hello';
}),
});
expectTypeOf(propsResult).toEqualTypeOf<Promise<string>>();
});
});

describe('serialization', () => {
Expand Down
Loading

0 comments on commit b59327e

Please sign in to comment.