Skip to content

Commit

Permalink
feat: improve TypeScript definitions (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanhofer authored Dec 10, 2021
1 parent af3ebeb commit 7986fc7
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 10 deletions.
62 changes: 61 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,61 @@
export default function svelteFsm(state: string | symbol, states: object): function;
type BaseState = string | symbol;

type BaseAction = string;

type BaseStates<State extends BaseState = BaseState> = Record<State, BaseActions>;

type Args = any[];

type LifecycleAction = (arg: {
from: BaseState | null;
to: BaseState;
event: BaseAction | null;
args: Args;
}) => void;

type ActionFunction = BaseState | ((...args: Args) => BaseState) | ((...args: Args) => void);

type BaseActions = {
_enter?: LifecycleAction;
_exit?: LifecycleAction;
[key: BaseAction]: ActionFunction;
};

type DetectFallBackState<State extends BaseState> = State extends '*' ? string : State;

type ExtractStates<States extends BaseStates> = DetectFallBackState<Exclude<keyof States, number>>;

type ExtractObjectValues<Object> = Object[keyof Object];

type GetActionFunctionMapping<Actions extends BaseActions> = {
[Key in Exclude<keyof Actions, '_enter' | '_exit'>]: Actions[Key] extends BaseState
? () => Actions[Key]
: Actions[Key];
};

type GetActionMapping<States extends BaseStates> = ExtractObjectValues<{
[Key in keyof States]: GetActionFunctionMapping<States[Key]>;
}>;

type ExtractActions<States extends BaseStates> = GetActionMapping<States>;

type Unsubscribe = () => void;

type Subscribe<S extends BaseState> = (callback: (state: S) => void) => Unsubscribe;

type StateMachine<State extends BaseState, Actions> = {
[Key in keyof Actions]: Actions[Key];
} & {
subscribe: Subscribe<State>;
};

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;

declare const svelteFsm: <Sts extends Readonly<BaseStates>, S extends ExtractStates<Sts>>(
state: S,
states: Sts
) => StateMachine<ExtractStates<Sts>, UnionToIntersection<ExtractActions<Sts>>>;

export default svelteFsm;
22 changes: 21 additions & 1 deletion package-lock.json

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
},
"types": "index.d.ts",
"scripts": {
"test": "mocha"
"test": "npm run test:units && npm run test:types",
"test:units": "mocha",
"test:types": "tsc --noEmit --target es6 test/*.ts"
},
"engines": {
"node": ">=14.0.0",
Expand All @@ -37,6 +39,7 @@
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^9.1.3",
"sinon": "^12.0.1"
"sinon": "^12.0.1",
"typescript": "^4.5.2"
}
}
105 changes: 105 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Verify type declarations found in index.d.ts by running:
* $ npx tsc --noEmit --target es6 test/types.ts
*/
import fsm from '../index.js';

// @ts-expect-error fsm expects 2 arguments (0 provided)
const invalid1 = fsm();
// @ts-expect-error fsm expects 2 arguments (1 provided)
const invalid2 = fsm('foo');
// @ts-expect-error fsm expects string or symbol for initial state (null provided)
const invalid3 = fsm(null, {});
// @ts-expect-error fsm expects string or symbol for initial state (number provided)
const invalid4 = fsm(1, {});
// @ts-expect-error fsm expects object for states (string provided)
const invalid5 = fsm('foo', 'bar');
// @ts-expect-error fsm expects initial state to match a defined state or fallback
const invalid6 = fsm('foo', {});

const invalid7 = fsm('foo', {
foo: {
// @ts-expect-error state expects action to be string or function (object provided)
bar: {},
// @ts-expect-error state expects action to be string or function (number provided)
baz: 1,
// @ts-expect-error state expects lifecycle action to be function (string provided)
_enter: 'bar'
}
});

// A simple, valid state machine
const valid1 = fsm('off', {
off: {
toggle: 'on'
},
on: {
toggle() {
return 'off';
}
}
});

// @ts-expect-error subscribe expects callback
valid1.subscribe();
const unsub = valid1.subscribe(() => {});
// @ts-expect-error unsubscribe expects no arguments
unsub('foo');
unsub();

// @ts-expect-error state machine expects valid event invocation
valid1.noSuchAction();

// @ts-expect-error toggle expects no arguments (1 provided)
valid1.toggle(1);
valid1.toggle();

const toggleResultValid: string | symbol = valid1.toggle();
// @ts-expect-error toggle returns string or symbol
const toggleResultInvalid: number = valid1.toggle();

// A state machine with fallback state (any initial state permitted)
const valid2 = fsm('initial', {
'*': {
foo: () => {}
}
});
valid2.foo();

// A state machine with overloaded action signatures
const valid3 = fsm('initial', {
'*': {
overloaded(one: number) {
return 'foo';
}
},
foo: {
overloaded(one: string, two: number) {}
}
});
// @ts-expect-error overloaded expects 1 or 2 args (0 provided)
valid3.overloaded();
// @ts-expect-error overloaded expects first argument as number
valid3.overloaded('string');
valid3.overloaded(1);
// @ts-expect-error overloaded expects first argument as string
valid3.overloaded(1, 2);
valid3.overloaded('string', 2);
// @ts-expect-error overloaded expects 1 or 2 args (3 provided)
valid3.overloaded(1, 2, 3);

// @ts-expect-error overloaded with single argument returns string | void
const overloadedResult1Invalid: void = valid3.overloaded(1);
const overloadedResult1Valid: string | void = valid3.overloaded(1);

// @ts-expect-error overloaded with two arguments returns only void
const overloadedResult2Invalid: string = valid3.overloaded('string', 1);
const overloadedResult2Valid: string | void = valid3.overloaded('string', 1);

// A state machine that uses symbols as a state keys
const valid4 = fsm(Symbol.for('foo'), {
[Symbol.for('foo')]: {
bar: Symbol.for('bar')
}
});
const symbolResultValid: string | symbol = valid4.bar();
12 changes: 6 additions & 6 deletions test.js → test/units.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assert } from 'chai';
import sinon from 'sinon';
import fsm from './index.js';
import fsm from '../index.js';

sinon.assert.expose(assert, { prefix: '' });

Expand Down Expand Up @@ -70,13 +70,13 @@ describe('a finite state machine', () => {
});

it('should call subscribe action handler when invoked with multiple args', () => {
const fn = sinon.fake()
const fn = sinon.fake();
machine.subscribe(fn, null);
assert.calledWithExactly(states.off.subscribe, fn, null);
});
});

describe('event invocations', function() {
describe('event invocations', () => {
let callback;
let unsubscribe;

Expand Down Expand Up @@ -145,8 +145,8 @@ describe('a finite state machine', () => {
from: null,
to: 'off',
event: null,
args: [] }
);
args: []
});
});

it('should call lifecycle actions with transition metadata', () => {
Expand Down Expand Up @@ -236,7 +236,7 @@ describe('a finite state machine', () => {
const kick = machine.kick.debounce(100, 1);
const cancelation = machine.kick.debounce(null);
clock.tick(100);
const state = await Promise.any([ kick, cancelation ]);
const state = await Promise.any([kick, cancelation]);
assert.notCalled(states.off.kick);
assert.equal('off', state);
});
Expand Down

0 comments on commit 7986fc7

Please sign in to comment.