Skip to content

Commit

Permalink
[major] rename isError to isAsyncError, restrict inputs to AsyncValu …
Browse files Browse the repository at this point in the history
…typeguard to prevent dev mistakes
  • Loading branch information
electrovir committed Apr 1, 2024
1 parent da4b3dd commit 4102740
Show file tree
Hide file tree
Showing 15 changed files with 102 additions and 89 deletions.
69 changes: 11 additions & 58 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@electrovir/element-vir-mono-repo",
"version": "21.0.0",
"version": "22.0.0",
"private": true,
"license": "(MIT or CC0 1.0)",
"author": {
Expand Down
2 changes: 1 addition & 1 deletion packages/element-book-example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@electrovir/element-book-example",
"version": "21.0.0",
"version": "22.0.0",
"private": true,
"license": "(MIT or CC0 1.0)",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions packages/element-book/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "element-book",
"version": "21.0.0",
"version": "22.0.0",
"keywords": [
"book",
"design system",
Expand Down Expand Up @@ -48,7 +48,7 @@
"lit-css-vars": "^3.0.9",
"spa-router-vir": "^3.0.4",
"typed-event-target": "^3.2.1",
"vira": "21.0.0"
"vira": "22.0.0"
},
"devDependencies": {
"@augment-vir/browser-testing": "^26.2.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/element-vir-example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@electrovir/element-vir-example",
"version": "21.0.0",
"version": "22.0.0",
"private": true,
"license": "(MIT or CC0 1.0)",
"author": {
Expand Down
1 change: 1 addition & 0 deletions packages/element-vir/configs/typedoc.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const typeDocConfig: Partial<TypeDocOptions> = {
excludeInternal: true,
excludeExternals: true,
intentionallyNotExported: [
'_AsyncPropClass',
'__class',
],
requiredToBeDocumented: [],
Expand Down
2 changes: 1 addition & 1 deletion packages/element-vir/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "element-vir",
"version": "21.0.0",
"version": "22.0.0",
"keywords": [
"custom",
"web",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {defineElementEvent} from '../properties/element-events';
import {StaticElementPropertyDescriptor} from '../properties/element-properties';
import {ElementVirStateSetup, stateSetupKey} from '../properties/element-vir-state-setup';
import {AsyncProp, AsyncValue, asyncProp} from './async-prop';
import {isError, isResolved} from './is-resolved.directive';
import {isAsyncError, isResolved} from './is-resolved.directive';
import {listen} from './listen.directive';
import {renderAsync} from './render-async.directive';

Expand Down Expand Up @@ -149,7 +149,7 @@ describe(asyncProp.name, () => {
circularReference,
});

if (isResolved(state.myAsyncProp.value) && !isError(state.myAsyncProp.value)) {
if (isResolved(state.myAsyncProp.value) && !isAsyncError(state.myAsyncProp.value)) {
assertTypeOf(state.myAsyncProp.value).toEqualTypeOf<number>();
}

Expand Down Expand Up @@ -437,7 +437,7 @@ describe(asyncProp.name, () => {
wasRendered: defineElementEvent<void>(),
},
renderCallback({dispatch, events, state}) {
if (isResolved(state.myAsyncProp.value) && !isError(state.myAsyncProp.value)) {
if (isResolved(state.myAsyncProp.value) && !isAsyncError(state.myAsyncProp.value)) {
assertTypeOf(state.myAsyncProp.value).toEqualTypeOf<number | undefined>();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {CallbackObservable, CallbackObservableInit} from 'observavir';
import {Constructor} from 'type-fest';
import {ElementVirStateSetup, stateSetupKey} from '../properties/element-vir-state-setup';

export type {AsyncValue} from 'observavir';

/** Class for constructing async props. Should not be referenced directly, use `AsyncProp` instead. */
export class _AsyncPropClass<Value, Params> extends CallbackObservable<Value, Params> {}
class _AsyncPropClass<Value, Params> extends CallbackObservable<Value, Params> {}

export type AsyncProp<Value, Params> = Omit<
_AsyncPropClass<Value, Params>,
Expand All @@ -19,6 +20,11 @@ export type AsyncProp<Value, Params> = Omit<
| 'listen'
>;

export const AsyncProp: Constructor<
AsyncProp<unknown, unknown>,
ConstructorParameters<typeof _AsyncPropClass<unknown, unknown>>
> = _AsyncPropClass;

export function asyncProp<Value, Params = void>(
init?: CallbackObservableInit<Value, Params>,
): ElementVirStateSetup<AsyncProp<Value, Params>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {itCases} from '@augment-vir/browser-testing';
import {assert} from '@open-wc/testing';
import {AsyncValue} from './async-prop';
import {isError, isResolved, resolvedOrUndefined} from './is-resolved.directive';
import {assertThrows, assertTypeOf} from 'run-time-assertions';
import {stateSetupKey} from '../properties/element-vir-state-setup';
import {AsyncValue, asyncProp} from './async-prop';
import {isAsyncError, isResolved, resolvedOrUndefined} from './is-resolved.directive';

describe(isResolved.name, () => {
itCases(isResolved, [
Expand All @@ -12,29 +14,66 @@ describe(isResolved.name, () => {
},
{
it: 'accepts errors',
input: new Error(),
input: new Error() as AsyncValue<any>,
expect: true,
},
{
it: 'accepts plain values',
input: {
stuff: 'hello',
},
} as AsyncValue<any>,
expect: true,
},
]);

it('properly type guards', () => {
const exampleAsyncProp = asyncProp({defaultValue: Promise.resolve('hi')})[stateSetupKey]();

if (isResolved(exampleAsyncProp.value)) {
assertTypeOf(exampleAsyncProp.value).toEqualTypeOf<string | Error>();
}
});

it("can't accidentally be passed AsyncProp instead of AsyncValue", () => {
const exampleAsyncProp = asyncProp({defaultValue: Promise.resolve('hi')})[stateSetupKey]();

assertThrows(
() => {
// @ts-expect-error: AsyncProp cannot be passed into `isResolved`
isResolved(exampleAsyncProp);
},
{
matchMessage: 'Pass AsyncProp.value',
},
);
});
});

describe(isError.name, () => {
describe(isAsyncError.name, () => {
it('does not block isResolved', () => {
const myValue = {} as AsyncValue<{something: 'crazy'}>;

if (isError(myValue)) {
if (isAsyncError(myValue)) {
assertTypeOf(myValue).toEqualTypeOf<Error>();
throw myValue;
} else if (isResolved(myValue)) {
assert.isObject(myValue);
}
});

it('fails if passed an AsyncProp', () => {
const exampleAsyncProp = asyncProp({defaultValue: Promise.resolve('hi')})[stateSetupKey]();

assertThrows(
() => {
// @ts-expect-error: AsyncProp cannot be passed into `isAsyncError`
isAsyncError(exampleAsyncProp);
},
{
matchMessage: 'Pass AsyncProp.value',
},
);
});
});

describe(resolvedOrUndefined.name, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import {isPromiseLike} from '@augment-vir/common';
import {AsyncValue} from './async-prop';
import {AsyncProp, AsyncValue} from './async-prop';

export function isResolved<Value extends AsyncValue<any>>(
asyncPropInput: Value,
): asyncPropInput is Exclude<Value, Promise<any>> {
return !isPromiseLike(asyncPropInput);
asyncValue: Value extends AsyncProp<any, any>
? 'Error: pass AsyncProp.value, not AsyncProp itself.'
: Value,
): asyncValue is Exclude<typeof asyncValue, Promise<any>> {
if (asyncValue instanceof AsyncProp) {
throw new TypeError('Pass AsyncProp.value, not AsyncProp itself.');
}

return !(asyncValue instanceof Promise);
}

export function isError(asyncPropInput: unknown): asyncPropInput is Error {
return asyncPropInput instanceof Error;
export function isAsyncError<Value extends AsyncValue<any>>(
asyncValue: Value extends AsyncProp<any, any>
? 'Error: pass AsyncProp.value, not AsyncProp itself.'
: Value,
): asyncValue is Extract<typeof asyncValue, Error> {
if (asyncValue instanceof AsyncProp) {
throw new TypeError('Pass AsyncProp.value, not AsyncProp itself.');
}
return asyncValue instanceof Error;
}

export function resolvedOrUndefined<Value extends AsyncValue<any>>(
asyncPropInput: Value,
): Exclude<Value, Promise<any>> | undefined {
if (isResolved(asyncPropInput)) {
return asyncPropInput;
asyncValue: Value extends AsyncProp<any, any>
? 'Error: pass AsyncProp.value, not AsyncProp itself.'
: Value,
): Exclude<typeof asyncValue, Promise<any>> | undefined {
if (isResolved(asyncValue)) {
return asyncValue;
} else {
return undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@electrovir/scripts",
"version": "21.0.0",
"version": "22.0.0",
"private": true,
"license": "(MIT or CC0 1.0)",
"author": {
Expand Down
Loading

0 comments on commit 4102740

Please sign in to comment.