-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript definitions #21
Comments
I would love to have a TypeScript definition! I need to get a working library definition for Flow too (right now the types are internal) so the timing is great. |
These aren't production-ready, but they're a start. declare module 'react-copy-write' {
import { Component } from 'react';
interface ProviderProps {
children: JSX.Element | JSX.Element[];
initialState?: object;
}
class Provider extends Component<ProviderProps, any> {}
type MutateFn<T> = (draft: T) => void;
type Mutator<T> = (mutator: MutateFn<T>) => void;
// "any" due to selector
type RenderFn<T> = (state: T | any, mutate: MutateFn<T>) => JSX.Element | JSX.Element[];
interface ConsumerProps<T> {
selector?: (state: T) => any;
render?: RenderFn<T>;
children?: RenderFn<T>;
}
class Consumer<T> extends Component<ConsumerProps<T>, any> {}
function create<T extends object>(state: T): {
Provider: new() => Provider,
Consumer: new() => Consumer<T>,
mutate: Mutator<T>,
};
export default create;
} |
@samhh Would you consider publishing these somewhere so that those of us using typescript can iterate on them? |
@davej I've created a formal type definition here that can be merged into DefinitelyTyped via PR once they're a bit further along. Feel free to contribute there until it's ready for PR; speaking of which, I've not submit anything to DT before so any feedback on that directory is helpful, particularly with regards to the "test". |
Im not a TypeScript user. Is it more common for type definitions to live in Definitely Typed or the repo itself? Happy to do do either. |
@aweary I'm only starting with typescript but I think it's better to include them in the repo itself if possible. DT is for the community to provide an escape hatch when a library decides not to include type definitions for whatever reason. |
@samhh Either have I! |
@samhh Ah ok. I was basing my answer on what I read on StackOverflow but TypeScript documentation is a much more canonical source. |
Note that the API has gone through some changes since @samhh's original attempt.
A couple other things:
|
@aweary Yes, using With regards to conditional mutability, I think we could use |
As I alluded to in the comments of the type definition I had two ideas to solve the
Of course, it's possible that there's a better way that would work today, but if so it's totally passed me by! 😛 |
@samhh: I'm seeing an error when using the second param (readable state) of the For example, in the code below: // store.ts
export interface IApp {
name: string;
url: string;
iconUrl?: string;
icons?: UploadFile[];
}
// actions.ts
export const updateApp = (props: Partial<IApp>) =>
mutate((draft, state) => {
const appIndex = state.selectedApp;
const app = state.apps[appIndex];
// ▼▼ TS ERROR ON LINE BELOW ▼▼
draft.apps[appIndex] = {
...app,
...props
};
}); The error that I'm getting is the following:
I could change the edit: Definition for |
@davej not sure about the TypeScript error, but the action you have there is doing more updates than necessary. You don't need to spread app or create a new object. Just mutate the existing one! export const updateApp = (props: Partial<IApp>) =>
mutate((draft, state) => {
const appIndex = state.selectedApp;
for (const key in props) {
draft.apps[appIndex][key] = props[key]
}
}); |
@aweary: That's true. I guess the issue that I was pointing out still stands though. There are cases where you may wish to assign something from |
Back to the import { Component } from 'react';
type MutateFn<T> = (draft: T, state: Readonly<T>) => void;
type Mutator<T> = (mutator: MutateFn<T>) => void;
type SelectorFn<T, U> = (state: T) => U;
// Below probably works 3.0+
// type RenderFn<T extends any[]> = (...state: Readonly<T>) => JSX.Element | JSX.Element[] | null;
type RenderFn<T extends any[]> = (
datum1: Readonly<T[0]>, datum2: Readonly<T[1]>, datum3: Readonly<T[2]>, datum4: Readonly<T[3]>, datum5: Readonly<T[4]>, datum6: Readonly<T[5]>, datum7: Readonly<T[6]>, datum8: Readonly<T[7]>, datum9: Readonly<T[8]>, datum10: Readonly<T[9]>,
) => JSX.Element | JSX.Element[] | null;
interface ConsumerProps<T, U extends any[]> {
// Below probably works 3.0+
// select?: [SelectorFn<T, U[0]>?, SelectorFn<T, U[1]>?, SelectorFn<T, U[2]>?, SelectorFn<T, U[3]>?, SelectorFn<T, U[4]>?, SelectorFn<T, U[5]>?, SelectorFn<T, U[6]>?, SelectorFn<T, U[7]>?, SelectorFn<T, U[8]>?, SelectorFn<T, U[9]>?];
select?:
[SelectorFn<T, U[0]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>, SelectorFn<T, U[4]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>, SelectorFn<T, U[4]>, SelectorFn<T, U[5]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>, SelectorFn<T, U[4]>, SelectorFn<T, U[5]>, SelectorFn<T, U[6]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>, SelectorFn<T, U[4]>, SelectorFn<T, U[5]>, SelectorFn<T, U[6]>, SelectorFn<T, U[7]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>, SelectorFn<T, U[4]>, SelectorFn<T, U[5]>, SelectorFn<T, U[6]>, SelectorFn<T, U[7]>, SelectorFn<T, U[8]>] |
[SelectorFn<T, U[0]>, SelectorFn<T, U[1]>, SelectorFn<T, U[2]>, SelectorFn<T, U[3]>, SelectorFn<T, U[4]>, SelectorFn<T, U[5]>, SelectorFn<T, U[6]>, SelectorFn<T, U[7]>, SelectorFn<T, U[8]>, SelectorFn<T, U[9]>];
render?: RenderFn<U>;
children?: RenderFn<U>;
}
declare class Consumer<T, U extends any[]> extends Component<ConsumerProps<T, U>> {}
interface ProviderProps<T> {
children: JSX.Element | JSX.Element[];
initialState?: Partial<T>;
}
declare class Provider<T> extends Component<ProviderProps<T>> {}
declare function create<T extends object>(state: T): {
Provider: new() => Provider<T>,
Consumer: new<U extends any[] = [T]>() => Consumer<T, U>,
createSelector: <U extends SelectorFn<T, any>>(selector: U) => U,
mutate: Mutator<T>,
};
export default create; I've added a generic to the import * as React from 'react';
import { render } from 'react-dom';
import createStore from 'react-copy-write';
const { Provider, Consumer, mutate, createSelector } = createStore({
user: {
avatar: {
src: '',
},
age: 42,
},
});
// Create reusable selectors for optimal performance
const selectAvatar = createSelector(state => state.user.avatar.src);
const selectAge = createSelector(state => state.user.age);
// Use state for lookup of preexisting data instead of draft (per underlying
// immer library)
const incrementAge = () => mutate((draft, state) => {
draft.user.age = state.user.age + 1;
});
const App = () => (
<Provider>
<div>
<Consumer<[ReturnType<typeof selectAvatar>, ReturnType<typeof selectAge>]> select={[selectAvatar, selectAge]}>
{(avatarSrc, age) => (
<>
<img src={avatarSrc} />
<p>User is {age} years old</p>
</>
)}
</Consumer>
<Consumer render={state => (
<button onClick={incrementAge}>Increment age</button>
)} />
</div>
</Provider>
);
render(<App />, document.body); Where I've gone for the very safe There are some caveats to this approach.
Oh, and I'm not sure because I hadn't yet used any selectors myself, but I think the |
^^ Please try and feedback the above before I submit a PR, don't want to introduce any more instability (a la the |
@samhh: Using it now, I've made a note to report back after a few days of use. |
@samhh: So far, so good. Anything in particular that I should be looking out for? |
@davej Aside from generally noticing any potential regressions, I'm just wondering if for others those specified caveats are worthwhile. I think they are but I want feedback before I thrust them upon everyone! |
Well I had trouble doing the following (based on your example): const selectApp = createSelector(state => state.apps[state.selectedApp]);
const BuildContainer: React.SFC = () => (
<Consumer<[ReturnType<typeof selectApp>]> select={[selectApp]}>
{(app) => <Build app={app} />}
</Consumer>
); The |
VSCode isn't giving me any errors, what's the build error? @davej |
I'm not getting a build error, just "Failed to compile" from typescript and tslint is passing. I'm using |
Hmm, I think that would suggest an issue with those; I've never personally seen TypeScript fail without telling you why, however verbose or cryptic. |
Yes, it does that on all errors. Usually TSLint will catch the error so it hasn't really been an issue for me so far. Created an issue on the repo of the boilerplate I'm using to see if I can get to the bottom of it. Jayphen/Boilerplate-CRA2-Typescript-Emotion#2 |
Ok I downgraded to
Perhaps this syntax just isn't supported in babel react for some reason. Do you know if there is an alternative syntax for doing this? |
Not that I know of. That syntax - allowing you to create generic React components, passing the arguments in via JSX - was introduced in TS 2.9.0. |
I would suggest to introduce a createConsumer(selector1,selector2....) function and remove the select attribute from the consumer. This way we could create type safe consumers and can remove any. Here are some demo typeings how that could look like: https://gist.github.com/lanwin/67ab9a1e6fd89dfb32eda525d4cd0def The code then could look like: const { createSelector, createConsumer } = createStore(storeData);
const fornameSelector = createSelector(state => ({
forname: state.name.split(" ")[0]
}));
const surenameSelector = createSelector(state => ({
surename: state.name.split(" ")[1]
}));
const SelectorConsumer = createConsumer(fornameSelector, surenameSelector);
export default function render() {
return (
<div>
<SelectorConsumer>
{state => <span>{state.surename}</span>}
</SelectorConsumer>
</div>
);
} |
@aweary what do you think about changing the Consumer to not longer take a select attribute and instead add a createConsumer function to create a consumer with specialized state. |
I'm using the following approach as a workaround: interface State {
// this is my state type
}
const { Provider, Consumer } = createState<State>(emptyState);
interface DataConsumerProps<T> {
selector: (state: State) => T;
children: (selectedState: T) => React.ReactNode;
}
// I use this component instead of `Consumer`
class DataConsumer<T> extends Component<DataConsumerProps<T>> {
public render() {
const { selector, children } = this.props;
return <Consumer select={[selector]}>{children}</Consumer>;
}
} Then in my views I use it like this: // Imagine this hypothetical state
interface State {
todoIds: string[];
todos: { [key: string]: TodoItem };
}
interface TodoViewProps {
todoId: string;
}
class TodoView extends Component<TodoViewProps> {
render() {
return (
// The type argument for DataConsumer has to be given explicitly, even though
// I kind of expected that it would be inferred from the return type of the selector.
<DataConsumer<TodoItem>
selector={state => state.todos[this.props.todoId]}
>
{todoItem => (
// ...
)}
</DataConsumer>
);
}
} The tradeoff of having a single selector is not a problem. I generally select little from the state. But even if it's more, I can bundled all I need in an object, instead of having more than one selector. Not sure though if this has implications vs. passing several selectors and receiving the results as different arguments in the render prop function. |
@gnapse the drawback of this approch is that you can not get type safty between the state in the store and the DataConsumer. You can basically use any type you want as T for DataConsumer without even noticing at compile time that it does not match with your State. |
Ah sorry it seems I am wrong. Forget the above. |
Would you be willing to include a TypeScript declaration file in this repo and publish it as part of the package? I'm interested in using this library in a TypeScript project and would be willing to work on a PR that uses the existing Flow types as the basis for the TS definitions.
The text was updated successfully, but these errors were encountered: