Skip to content

Commit

Permalink
detect name collisions
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi committed Apr 18, 2024
1 parent 107606c commit f6a2d4f
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 41 deletions.
69 changes: 48 additions & 21 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier"
],
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
"plugin:@typescript-eslint/recommended",
"airbnb",
Expand All @@ -28,7 +25,10 @@
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }],
"react/jsx-filename-extension": [
"error",
{ "extensions": [".js", ".tsx"] }
],
"react/prop-types": "off",
"react/jsx-one-expression-per-line": "off",
"import/extensions": ["error", "never"],
Expand All @@ -41,24 +41,51 @@
"prefer-object-spread": "off",
"no-use-before-define": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"varsIgnorePattern": "^(createElement|Fragment)$"
}],
"@typescript-eslint/no-unused-vars": [
"error",
{
"varsIgnorePattern": "^(createElement$|Fragment$|_)"
}
],
"no-redeclare": "off",
"react/function-component-definition": ["error", { "namedComponents": "arrow-function" }]
"react/function-component-definition": [
"error",
{ "namedComponents": "arrow-function" }
],
"no-restricted-syntax": [
"error",
{
"selector": "ForInStatement",
"message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."
},
{
"selector": "LabeledStatement",
"message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."
},
{
"selector": "WithStatement",
"message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize."
}
]
},
"overrides": [{
"files": ["__tests__/**/*"],
"env": {
"jest": true
"overrides": [
{
"files": ["__tests__/**/*"],
"env": {
"jest": true
},
"rules": {
"import/no-extraneous-dependencies": [
"error",
{ "devDependencies": true }
]
}
},
"rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
}
}, {
"files": ["examples/**/*"],
"rules": {
"import/no-extraneous-dependencies": "off"
{
"files": ["examples/**/*"],
"rules": {
"import/no-extraneous-dependencies": "off"
}
}
}]
]
}
125 changes: 125 additions & 0 deletions __tests__/02_type_spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { expectType } from 'ts-expect';
import type { TypeEqual } from 'ts-expect';

import { createSlice, withSlices } from '../src/index';

describe('type spec', () => {
it('single slice', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
},
});
expectType<
TypeEqual<
{
name: 'count';
value: number;
actions: {
inc: () => (prev: number) => number;
};
},
typeof countSlice
>
>(true);
});

it('detect name collisions', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
},
});
const anotherCountSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
},
});
expectType<never>(withSlices(countSlice, anotherCountSlice));
});

it('detect name collisions with actions', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
},
});
const anotherCountSlice = createSlice({
name: 'anotherCount',
value: 0,
actions: {
count: () => (prev) => prev + 1,
},
});
expectType<never>(withSlices(countSlice, anotherCountSlice));
});

it('detect args collisions', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: (s: string) => (prev) => prev + (s ? 2 : 1),
},
});
const anotherCountSlice = createSlice({
name: 'anotherCount',
value: 0,
actions: {
inc: (n: number) => (prev) => prev + n,
},
});
expectType<never>(withSlices(countSlice, anotherCountSlice));
expectType<never>(withSlices(anotherCountSlice, countSlice));
});

it('no args collisions', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
},
});
const anotherCountSlice = createSlice({
name: 'anotherCount',
value: 0,
actions: {
anotherInc: (n: number) => (prev) => prev + n,
},
});
expectType<(...args: never[]) => unknown>(
withSlices(countSlice, anotherCountSlice),
);
expectType<(...args: never[]) => unknown>(
withSlices(anotherCountSlice, countSlice),
);
});

it('detect args collisions (overload case)', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
},
});
const anotherCountSlice = createSlice({
name: 'anotherCount',
value: 0,
actions: {
inc: (n: number) => (prev) => prev + n,
},
});
expectType<never>(withSlices(countSlice, anotherCountSlice));
expectType<never>(withSlices(anotherCountSlice, countSlice));
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-expect": "^1.3.0",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

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

85 changes: 65 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,95 @@
/* eslint no-restricted-syntax: off */
type ParametersIf<T> = T extends (...args: infer Args) => any ? Args : never;

type SliceConfig<
Name extends string,
SliceValue,
Value,
Actions extends {
[actionName: string]: (
...args: never[]
) => (slice: SliceValue) => SliceValue;
[actionName: string]: (...args: never[]) => (slice: Value) => Value;
},
> = {
name: Name;
value: SliceValue;
value: Value;
actions: Actions;
};

type InferState<Configs> = Configs extends [
SliceConfig<infer Name, infer SliceValue, infer Actions>,
SliceConfig<infer Name, infer Value, infer Actions>,
...infer Rest,
]
? { [name in Name]: SliceValue } & {
? { [name in Name]: Value } & {
[actionName in keyof Actions]: (
...args: Parameters<Actions[actionName]>
) => void;
} & InferState<Rest>
: unknown;

type IsDupicated<Name, Names extends unknown[]> = Names extends [
infer One,
...infer Rest,
]
? One extends Name
? true
: IsDupicated<Name, Rest>
: false;

type HasDuplicatedNames<
Configs,
Names extends string[] = [],
> = Configs extends [
SliceConfig<infer Name, infer _Value, infer Actions>,
...infer Rest,
]
? Name extends Names[number]
? true
: IsDupicated<keyof Actions, Names> extends true
? true
: HasDuplicatedNames<Rest, [Name, ...Names]>
: false;

type HasDuplicatedArgs<Configs, State> = Configs extends [
SliceConfig<infer _Name, infer _Value, infer Actions>,
...infer Rest,
]
? {
[actionName in keyof State]: ParametersIf<State[actionName]>;
} extends {
[actionName in keyof Actions]: Parameters<Actions[actionName]>;
}
? HasDuplicatedArgs<Rest, State>
: true
: false;

type IsValidConfigs<Configs> =
HasDuplicatedNames<Configs> extends true
? false
: HasDuplicatedArgs<Configs, InferState<Configs>> extends true
? false
: true;

export function createSlice<
Name extends string,
SliceValue,
Value,
Actions extends {
[actionName: string]: (
...args: never[]
) => (slice: SliceValue) => SliceValue;
[actionName: string]: (...args: never[]) => (slice: Value) => Value;
},
>(config: SliceConfig<Name, SliceValue, Actions>) {
>(config: SliceConfig<Name, Value, Actions>) {
return config;
}

export function withSlices<Configs extends SliceConfig<string, unknown, any>[]>(
...configs: Configs
): (
set: (
fn: (prevState: InferState<Configs>) => Partial<InferState<Configs>>,
) => void,
) => InferState<Configs> {
return (set) => {
): IsValidConfigs<Configs> extends true
? (
set: (
fn: (prevState: InferState<Configs>) => Partial<InferState<Configs>>,
) => void,
) => InferState<Configs>
: never {
return ((
set: (
fn: (prevState: InferState<Configs>) => Partial<InferState<Configs>>,
) => void,
) => {
const state: any = {};
const actionNameSet = new Set<string>();
for (const config of configs) {
Expand All @@ -69,5 +114,5 @@ export function withSlices<Configs extends SliceConfig<string, unknown, any>[]>(
};
}
return state;
};
}) as never;
}

0 comments on commit f6a2d4f

Please sign in to comment.