Skip to content

Commit

Permalink
feat: checked cast with TypedMatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Sep 30, 2023
1 parent ae21a7e commit 94e2ff6
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/internal/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from './config.js';
export * from './debug.js';
export * from './utils.js';
export * from './method-tools.js';
export * from './typeCheck.js';
export * from './typeGuards.js';
19 changes: 19 additions & 0 deletions packages/internal/src/typeCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// @ts-check
import { mustMatch as typelessMustMatch } from '@endo/patterns';

/** @type {import('./types.js').MustMatch} */
export const mustMatch = typelessMustMatch;

/**
* @template {import('./types.js').TypedMatcher} M
* @param {unknown} specimen
* @param {M} patt
* @returns {import('./types.js').MatcherType<M>}
*/
export const cast = (specimen, patt) => {
// mustMatch throws if they don't, which means that `cast` also narrows the
// type but a function can't both narrow and return a type. That is by design:
// https://github.com/microsoft/TypeScript/issues/34636#issuecomment-545025916
mustMatch(specimen, patt);
return specimen;
};
16 changes: 16 additions & 0 deletions packages/internal/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable max-classes-per-file */
import type { Matcher } from '@endo/patterns';

export declare class Callback<I extends (...args: unknown[]) => any> {
private iface: I;

Expand All @@ -18,3 +20,17 @@ export declare class SyncCallback<

public isSync: true;
}

declare const typeTag: unique symbol;
export declare type TypedMatcher<T = unknown> = Matcher & {
readonly [typeTag]: T;
};
export declare type MatcherType<M> = M extends TypedMatcher<infer T>
? T
: unknown;

export declare type MustMatch = <M>(
specimen: unknown,
matcher: M,
label?: string,
) => asserts specimen is MatcherType<M>;
28 changes: 28 additions & 0 deletions packages/internal/test/test-typeCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @ts-check
import test from 'ava';

import { M } from '@endo/patterns';
import { cast, mustMatch } from '../src/typeCheck.js';

const Mstring = /** @type {import('../src/types.js').TypedMatcher<string>} */ (
M.string()
);

const unknownString = /** @type {unknown} */ ('');

test('cast', t => {
// @ts-expect-error unknown type
unknownString.length;
// @ts-expect-error not any
cast(unknownString, Mstring).missing;
cast(unknownString, Mstring).length;
t.pass();
});

test('mustMatch', t => {
// @ts-expect-error unknown type
unknownString.length;
mustMatch(unknownString, Mstring);
unknownString.length;
t.pass();
});

0 comments on commit 94e2ff6

Please sign in to comment.