Skip to content

Commit

Permalink
feat(recoil) add Recoil typings
Browse files Browse the repository at this point in the history
  • Loading branch information
Moshe Kolodny committed May 26, 2020
1 parent c3ba003 commit 7bae698
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
84 changes: 84 additions & 0 deletions types/recoil/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Type definitions for recoil 0.0
// Project: https://github.com/facebookexperimental/recoil#readme
// Definitions by: Moshe Kolodny <https://github.com/kolodny>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

import React = require('react');

export {};

type Getter = <U>(u: RecoilValue<U>) => U;
type Setter = <U>(coil: RecoilValue<U>, value: U | ((u: U) => U)) => void;

type SelectorGet<T> = (callback: { get: Getter }) => T;

interface SelectorSet<U> {
set: Setter;
get: Getter;
}

interface SelectorParameter<T> {
key: string;
get: SelectorGet<T>;
set?: <U extends T>(options: SelectorSet<U>, newValue: U) => void;
}

export const RecoilRoot: React.ComponentType;

interface Atom<T> {
readonly __type__: unique symbol;
key: string;
}

interface Selector<T> {
readonly __type__: unique symbol;
key: string;
}

type RecoilValue<T> = Atom<T> | Selector<T>;

export function atom<T>(options: { key: string; default: T }): Atom<T>;
export function selector<T>(options: SelectorParameter<T>): Selector<T>;

export function isRecoilValue(x: any): x is RecoilValue<any>;

type ReactState<T> = [T, React.Dispatch<React.SetStateAction<T>>];

export function useRecoilState<T>(recoilValue: RecoilValue<T>): ReactState<T>;
export function useRecoilValue<T>(recoilValue: RecoilValue<T>): ReactState<T>[0];
export function useSetRecoilState<T>(recoilValue: RecoilValue<T>): ReactState<T>[1];

export function useResetRecoilState(recoilValue: RecoilValue<any>): () => void;

type Loadable<T> =
| {
state: 'hasValue';
contents: T;
getValue: () => Promise<T>;
toPromise: () => Promise<T>;
}
| {
state: 'loading';
contents: Promise<T>;
/** When state is 'loading' getValue throws a Promise */
getValue: () => never;
toPromise: () => Promise<T>;
}
| {
state: 'hasError';
contents: Error;
/** When state is 'hasError' getValue() throws the error */
getValue: () => never;
toPromise: () => Promise<never>;
};
export function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T>;
export function useRecoilStateLoadable<T>(recoilValue: RecoilValue<T>): [Loadable<T>, ReactState<T>];

interface CallbackInterface {
getPromise: <T>(recoilValue: RecoilValue<T>) => Promise<T>;
getLoadable: <T>(recoilValue: RecoilValue<T>) => Loadable<T>;
set: Setter;
reset: (recoilValue: RecoilValue<any>) => void;
}

export function useRecoilCallback(callback: (callbackInterface: CallbackInterface) => void): () => void;
187 changes: 187 additions & 0 deletions types/recoil/recoil-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {
atom,
selector,
useRecoilState,
useRecoilCallback,
isRecoilValue,
useResetRecoilState,
useRecoilValue,
useSetRecoilState,
useRecoilValueLoadable,
useRecoilStateLoadable,
} from 'recoil';

function describe(desc: string, fn: () => void) {}
function it(desc: string, fn: () => void) {}

describe('atom', () => {
it('has a key prop', () => {
const a = atom({
key: 'a',
default: '123',
});
a; // $ExpectType Atom<string>
a.key; // $ExpectType string
});
});

describe('selector', () => {
it('has a get prop', () => {
const s = selector({
key: 's',
get: () => 123
});
s; // $ExpectType Selector<number>
s.key; // $ExpectType string
});

// Yo dawg
it('gets the `get` callback to get', () => {
const a = atom({ key: 'a', default: 123 });
const s = selector({
key: 's',
get: ({ get }) => {
const got = get(a); // $ExpectType number
return `${got}`;
}
});
s; // $ExpectType Selector<string>
});

it('gets the `get` callback to set', () => {
const a = atom({ key: 'a', default: 123 });
const s = selector({
key: 's',
get: () => 123,
set: ({ get }) => {
const got = get(a); // $ExpectType number
}
});
});

it('gets the `set` callback to set', () => {
const a = atom({ key: 'a', default: 123 });
const s = selector({
key: 's',
get: () => 123,
set: ({ set }) => {
set(a, '123'); // $ExpectError
set(a, 321);
}
});
});
});

describe('atom <-> selector uniqueness', () => {
it(`can't assign atom to selector or vice versa`, () => {
let a = atom({ key: 'a', default: '123' });
let s = selector({ key: 'a', get: () => '123' });
a = s; // $ExpectError
s = a; // $ExpectError
});
});

describe('useRecoilState', () => {
it('works on atoms', () => {
const a = atom({ key: 'a', default: '123' });
const [aState, setAState] = useRecoilState(a);
aState; // $ExpectType string
setAState; // $ExpectType Dispatch<SetStateAction<string>>
});
});

describe('useRecoilValue', () => {
it('works on atoms', () => {
const a = atom({ key: 'a', default: '123' });
const aState = useRecoilValue(a);
aState; // $ExpectType string
});
});

describe('useSetRecoilState', () => {
it('works on atoms', () => {
const a = atom({ key: 'a', default: '123' });
const setAState = useSetRecoilState(a);
setAState; // $ExpectType Dispatch<SetStateAction<string>>
});
});

describe('isRecoilValue', () => {
it('is a typeguard', () => {
const x = {} as any;
if (isRecoilValue(x)) {
x; // $ExpectType RecoilValue<any>
}
});
});

describe('useResetRecoilState', () => {
it('is a void fn', () => {
const a = atom({ key: 'a', default: '123' });
const fn = useResetRecoilState(a);
fn; // $ExpectType () => void
});
});

describe('useRecoilValueLoadable', () => {
it(`has a 'hasValue' state`, () => {
const a = atom({ key: 'a', default: '123' });
const loadableState = useRecoilValueLoadable(a);
if (loadableState.state === 'hasValue') {
loadableState.contents; // $ExpectType string
loadableState.getValue; // $ExpectType () => Promise<string>
loadableState.state; // $ExpectType "hasValue"
loadableState.toPromise; // $ExpectType () => Promise<string>
}
});

it(`has a 'loading' state`, () => {
const a = atom({ key: 'a', default: '123' });
const loadableState = useRecoilValueLoadable(a);
if (loadableState.state === 'loading') {
loadableState.contents; // $ExpectType Promise<string>

// DocComment should be: "When state is 'loading' getValue throws a Promise<T>"
loadableState.getValue; // $ExpectType () => never
loadableState.state; // $ExpectType "loading"
loadableState.toPromise; // $ExpectType () => Promise<string>
}
});

it(`has a 'hasError' state`, () => {
const a = atom({ key: 'a', default: '123' });
const loadableState = useRecoilValueLoadable(a);
if (loadableState.state === 'hasError') {
loadableState.contents; // $ExpectType Error
// DocComment should be: "When state is 'hasError' getValue() throws the error"
loadableState.getValue; // $ExpectType () => never
loadableState.state; // $ExpectType "hasError"
loadableState.toPromise; // $ExpectType () => Promise<never>
}
});
});

describe('useRecoilValueLoadable', () => {
it(`has a 'hasValue' state`, () => {
const a = atom({ key: 'a', default: '123' });
const [loadableState, [state, setState]] = useRecoilStateLoadable(a);

// See `useRecoilValueLoadable` suite above for better tests.
loadableState; // $ExpectType Loadable<string>

state; // $ExpectType string
setState; // $ExpectType Dispatch<SetStateAction<string>>
});
});

describe('useRecoilCallback', () => {
it('gets the correct arguments in the callback', () => {
const itemsInCart = atom({ key: 'a', default: 123 });
const logCartItems = useRecoilCallback(async ({getPromise}) => {
const numItemsInCart = await getPromise(itemsInCart); // $ExpectType number

/* console.log( */ `items in cart: ${numItemsInCart}` /* ) */;
});
logCartItems; // $ExpectType () => void
});
});
24 changes: 24 additions & 0 deletions types/recoil/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": [
"es6"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"baseUrl": "../",
"typeRoots": [
"../"
],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true
},
"files": [
"index.d.ts",
"recoil-tests.ts"
]
}
1 change: 1 addition & 0 deletions types/recoil/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "extends": "dtslint/dt.json" }

0 comments on commit 7bae698

Please sign in to comment.