24
loading...
This website collects cookies to deliver better user experience
Teaful
reached more than 500 GitHub ⭐️ Stars, the library and his community are growing fast.TypeScript
at Teaful
is a big step on that way.any
was useless. So, we started implementing custom types.microbundle
, they provide a flag to avoid auto-generate types, --no-generateTypes
. Microbundle, according to docs, generally respect your TypeScript config at tsconfig.json
(you can read more about here), but at this moment we don't need a specific configuration for TypeScript
package.json
where are our custom types with "types": "folder/index.d.ts"
..d.ts
, generally you'll put this file on dist
folder. Now here you can add your custom types.Teaful
and why some decisions were taken, if you're reading this to know how to add custom types to your js library and already know about TypeScript
, feel free to skip this section.store
is where Teaful
saves data, is a key-value object (you can have more than one store). Easy to type:type initialStoreType = Record<string, any>;
Teaful
I strongly recommend visit the README at github. useState
from React
. Let's see an example:const [username, setUsername] = useStore.username();
useStore
returns an array of two elements (Yes! Like useState!), the element in the store and the function to update it. type HookReturn<T> = [T, (value: T | ((value: T) => T | undefined | null) ) => void];
HookReturn
which gets a generic type we called 'T
' (from Type, but you can use any name). useStore
), where first element is T
, because we want to return a value with specific type that we don't know at the moment of creating the type, but we want to ensure, for example, that the setter function (the second element on this tuple) will get the same type we are using for the first element as param.(value: T | ((value: T) => T | undefined | null) ) => void
() => void
), but accepts one param (value: T | ((value: T) => T | undefined | null)
), and this param could be a value of type T
, or a function that get a value of type T
and returns null
, undefined
or a value of type T
((value: T) => T | undefined | null
). const [counter, setCounter] = useStore.counter();
//allowed by T
setCounter(counter+1);
//allowed by ((value: T) => T | undefined | null)
setCounter((counter) => counter*2))
setCounter((counter) => undefined)
setCounter((counter) => null)
useStore.[newProperty]()
. This accepts two optional params, first for initialValue
, and the second one is for updateValue
(a function to update the store property indicated with the proxy
). The hook looks easy to create here:type Hook<S> = (
initial?: S,
onAfterUpdate?: afterCallbackType<S>
) => HookReturn<S>;
onAfterUpdate
, is a function with two params: store
before and after the changes, both will be same type, extending our initialStore
type.type afterCallbackType<S extends initialStoreType> = (
param: { store: S; prevStore: S; }
) => void
Hook
will return a tuple [property,setter]
, so indeed, we're going to return our custom type HookReturn
with our generic type. If we create a number, have sense to take care about number type in all places, for the initial value, the returned tuple... etc.const { withStore } = createStore({ count: 0 });
class Counter extends Component {
render() {
const [store, setStore] = this.props.store;
return (
// [...]
);
}
}
// Similar to useStore()
const CounterWithStore = withStore(Counter);
withStore
wraps a Component
and returns the component with a prop called store. A second parameter for initial value is allowed, and a third one for onAfterUpdate
callback.type HocFunc<S, R extends React.ComponentClass = React.ComponentClass> = (
component: R,
initial?: S,
onAfterUpdate?: afterCallbackType<S>
) => R;
onAfterUpdate
(both will use same generic, but onAfterUpdate
will have a specific type, explained later) and the other one for React
component to wrap that would be the same for the return, because we want the same component but with a new prop called store.R
type, is extending React.ComponentClass
(type provided by React
). This means that we are taking profit from that type and including it in our generic type called R
. onAfterUpdate
. Here we need a function with two params store before and after the changes, both will be same type, extending our initialStore
type. Same as first hook, we reuse same type for all callbacks paramsexport type Hoc<S> = { store: HookReturn<S> };
Teaful
provides a helper called getStore
, like useStore but:useStore
type, we return the same but we want to ensure we don't accept a second param as callback. Let's create another one:type HookDry<S> = (initial?: S) => HookReturn<S>;
useStore
, getStore
and withStore
:type getStoreType<S extends initialStoreType> = {
[key in keyof S]: S[key] extends initialStoreType
? useStoreType<S[key]> & HookDry<S[key]> : HookDry<S[key]>;
};
type useStoreType<S extends initialStoreType> = {
[key in keyof S]: S[key] extends initialStoreType
? useStoreType<S[key]> & Hook<S[key]> : Hook<S[key]>;
};
type withStoreType<S extends initialStoreType> = {
[key in keyof S]: S[key] extends initialStoreType
? withStoreType<S[key]> & HocFunc<S>
: HocFunc<S>;
};
Typescript
, is used for conditional-types. The logic shared in three types is, get a generic type (S
, that extends our initialStoreType
), then get a key
that must be on S
(the property should exists on our store). withStoreType<S[key]> & HocFunc<S>
is a Intersection type. According to TypeScript documentation "An intersection type combines multiple types into one". So if S[key]
extends initialStore
, we set the intersection type, if not, the hook/hoc type only.Teaful
, the masterpiece:function createStore<S extends initialStoreType>(
initial?: S,
afterCallback?: afterCallbackType<S>
): {
getStore: HookDry<S> & getStoreType<S>;
useStore: Hook<S> & useStoreType<S>;
withStore: HocFunc<S> & withStoreType<S>;
};
tsconfig.json
or whatever.d.ts
file.package.json
the place of that file with property "types"
.