[@types/redux-orm] add six overloads for createSelector and fix invalid return type of createReducer (#36738)

This commit is contained in:
Tomasz Zabłocki 2019-07-10 01:21:06 +02:00 committed by Armando Aguirre
parent bd4aef3a75
commit cef01b83fb
4 changed files with 212 additions and 60 deletions

View File

@ -37,7 +37,7 @@ export interface SerializableMap {
* - {@link MutableQuerySet} - for many-to-many relations
* - {@link QuerySet} - for reverse side of foreign keys
*/
export type ModelField = MutableQuerySet | QuerySet | SessionBoundModel | Serializable;
export type ModelField = QuerySet | SessionBoundModel | Serializable;
/**
* Map of fields restriction to supported field types.
@ -72,7 +72,7 @@ export type IdOrModelLike<M extends Model> = IdType<M> | {getId(): IdType<M>; };
* logic by defining prototype methods (without `static` keyword).
* @borrows {@link QuerySet.filter} as Model#filter
*/
export default class Model<MClass extends typeof AnyModel = any, Fields extends ModelFieldMap = any> {
export default class Model<MClass extends typeof AnyModel = typeof AnyModel, Fields extends ModelFieldMap = any> {
/**
* A string constant identifying specific Model, necessary to retain the shape of state and relations through transpilation steps
*/
@ -122,7 +122,7 @@ export default class Model<MClass extends typeof AnyModel = any, Fields extends
*
* @return a reference to the plain JS object in the store
*/
readonly ref: Ref<InstanceType<MClass>>;
readonly ref: Ref<this>;
/**
* Creates a Model instance from it's properties.
@ -402,7 +402,7 @@ export type IdType<M extends Model> = IdKey<M> extends infer U
/**
* Type of {@link Model.ref} / database entry for a particular Model type
*/
export type Ref<M extends Model> = {
export type Ref<M extends AnyModel> = {
[K in keyof RefFields<M>]: ModelFields<M>[K] extends AnyModel ? IdType<ModelFields<M>[K]> : RefFields<M>[K]
};
@ -412,7 +412,7 @@ export type Ref<M extends Model> = {
* - declared Model field type - if propertyName belongs to declared Model fields
* - any serializable value - if propertyName is not among declared Model fields
*/
export type RefPropOrSimple<M extends Model, K extends string> = K extends keyof RefFields<M>
export type RefPropOrSimple<M extends AnyModel, K extends string> = K extends keyof RefFields<M>
? Ref<M>[K]
: Serializable;
@ -477,7 +477,7 @@ export type ModelClass<M extends AnyModel> = ReturnType<M['getClass']>;
/**
* @internal
*/
export type ModelFields<M extends Model> = [ConstructorParameters<ModelClass<M>>] extends [[infer U]]
export type ModelFields<M extends Model> = ConstructorParameters<ModelClass<M>> extends [infer U]
? U extends ModelFieldMap
? U
: never

View File

@ -10,7 +10,7 @@ import Session, { OrmSession } from './Session';
* - ORM branch state type
*/
export type IndexedModelClasses<
T extends { [k in keyof T]: typeof AnyModel },
T extends { [k in keyof T]: typeof AnyModel } = {},
K extends keyof T = Extract<keyof T, T[keyof T]['modelName']>
> = { [k in K]: T[K] };
@ -88,7 +88,7 @@ export class ORM<I extends IndexedModelClasses<any>, ModelNames extends keyof I
*
* @return a new {@link Session} instance
*/
session(state: OrmState<I>): OrmSession<I>;
session(state?: OrmState<I>): OrmSession<I>;
/**
* Begins an mutable database session.

View File

@ -125,36 +125,36 @@ const sessionFixture = () => {
return orm.session(orm.getEmptyState());
};
// inferred optionality of ModelType.create argument properties
const argOptionalityAtModelCreation = () => {
const { Book, Publisher, Person } = sessionFixture();
// argOptionalityAtModelCreation - inferred optionality of ModelType.create argument properties
(() => {
const { Book, Publisher } = sessionFixture();
/**
* 1.A. `number` Model identifiers are optional due to built-in incremental sequencing of numeric identifiers
* @see {@link PublisherFields.index}
*/
const publisher = Publisher.create({ name: 'P1' });
Publisher.create({ name: 'P1' });
/**
* 1.B. `string` identifiers are mandatory
*/
const stringIdMissing = Book.create({ publisher: 1, coverArt: 'foo.bmp' }); // $ExpectError
Book.create({ publisher: 1, coverArt: 'foo.bmp' }); // $ExpectError
/**
* 2. non-relational fields with corresponding descriptors that contain defined `getDefault` callback: (`attr({ getDefault: () => 'empty.png' })`)
* @see {@link Book#fields.coverArt}
*/
const book2 = Book.create({ title: 'B2', publisher: 1 });
Book.create({ title: 'B2', publisher: 1 });
/**
* 3. both attribute and relational fields where corresponding ModelFields interface property has optional (`?`) modifier
* @see {@link BookFields.authors}
*/
const book1 = Book.create({ title: 'B1', publisher: 1, coverArt: 'foo.bmp' });
};
Book.create({ title: 'B1', publisher: 1, coverArt: 'foo.bmp' });
})();
// ModelFields contribute to type constraints within ModelType.create arguments
const argPropertyTypeRestrictionsOnCreate = () => {
// argPropertyTypeRestrictionsOnCreate - ModelFields contribute to type constraints within ModelType.create arguments
(() => {
const { Book, Publisher, Person } = sessionFixture();
/** Keys of declared model fields interface contribute strict requirements regarding corresponding property types */
@ -180,7 +180,7 @@ const argPropertyTypeRestrictionsOnCreate = () => {
Book.create({ title: 'B1', publisher: publisherModel, authors: [authorModel] });
Book.create({
title: 'B1',
publisher: publisherModel.index ,
publisher: publisherModel.index,
authors: [authorModel, 'A1', authorModel, authorModel.ref.id]
});
@ -189,10 +189,10 @@ const argPropertyTypeRestrictionsOnCreate = () => {
Book.create({ title: 'B1', publisher: publisherModel.ref, authors: [publisherModel.ref, 'A1'] }); // $ExpectError
Book.create({ title: 'B1', publisher: { index: 'P1 ' } }); // $ExpectError
Book.create({ title: 'B1', publisher: { index: 0 }, authors: [authorModel, true] }); // $ExpectError
};
})();
// ModelFields contribute to type constraints within ModelType.create arguments
const argPropertyTypeRestrictionsOnUpsert = () => {
// argPropertyTypeRestrictionsOnUpsert - ModelFields contribute to type constraints within ModelType.create arguments
(() => {
const { Book, Publisher, Person } = sessionFixture();
/** Upsert requires id to be provided */
@ -228,45 +228,46 @@ const argPropertyTypeRestrictionsOnUpsert = () => {
Book.create({ title: 'B1', publisher: publisherModel.ref, authors: [publisherModel.ref, 'A1'] }); // $ExpectError
Book.create({ title: 'B1', publisher: { index: 'P1 ' } }); // $ExpectError
Book.create({ title: 'B1', publisher: { index: 0 }, authors: [authorModel, true] }); // $ExpectError
};
})();
// restriction of allowed ORM.register args
const restrictRegisterArgsToSchemaModels = () => {
(() => {
const incompleteSchema = { Book, Authorship, Person };
const orm = new ORM<typeof incompleteSchema>();
orm.register(Book, Authorship, Person, Publisher); // $ExpectError
};
})();
// inference of ORM branch state type
const inferOrmBranchEmptyState = () => {
(() => {
const emptyState = ormFixture().getEmptyState();
const bookTableState = emptyState.Book; // $ExpectType TableState<typeof Book>
const bookItemsById = emptyState.Book.itemsById; // $ExpectType { readonly [K: string]: Ref<Book>; }
const authorshipMetaState = emptyState.Authorship.meta.maxId; // $ExpectType number
const bookMetaState = emptyState.Book.meta.maxId; // $ExpectType number | null
};
})();
// indexing session instance using registered Model.modelName returns narrowed Model class
const sessionInstanceExtendedWithNarrowedModelClasses = () => {
// sessionInstanceExtendedWithNarrowedModelClasses - indexing session instance using registered Model.modelName returns narrowed Model class
(() => {
const { Book, Person, Publisher } = sessionFixture();
// $ExpectType { Book: ModelType<Book>; Person: ModelType<Person>; Publisher: ModelType<Publisher>; }
const sessionBoundModels = { Book, Person, Publisher };
};
return { ...sessionBoundModels };
})();
// IdKey and IdType mapped types support for valid identifier configurations
const idInferenceAndCustomizations = () => {
(() => {
type ExtractId<M extends Model> = [IdKey<M>, IdType<M>];
type ImplicitDefault = ExtractId<Authorship>; // $ExpectType ["id", number]
type CustomKey = ExtractId<Publisher>; // $ExpectType ["index", number]
type CustomType = ExtractId<Person>; // $ExpectType ["id", string]
type CustomKeyAndType = ExtractId<Book>; // $ExpectType ["title", string]
};
})();
// Model#create result retains custom properties supplied during call
const customInstanceProperties = () => {
(() => {
const { Book } = sessionFixture();
const basicBook = Book.create({ title: 'book', publisher: 1 });
@ -287,30 +288,31 @@ const customInstanceProperties = () => {
type customBookKeys = Exclude<keyof typeof extendedBook, keyof Model>; // $ExpectType "title" | "coverArt" | "publisher" | "authors" | "customProp"
const extendedBookTitle = extendedBook.title; // $ExpectType string
const instanceCustomProp = extendedBook.customProp; // $ExpectType { foo: number; bar: boolean; }
};
})();
// reducer API is intact
const standaloneReducerFunction = () => {
(() => {
const orm = ormFixture();
type StateType = OrmState<Schema>;
const reducerAddItem = (state: StateType, action: CreateBookAction): StateType => {
return (state: StateType, action: CreateBookAction): StateType => {
const session = orm.session(state);
session.Book.create(action.payload);
return session.state;
};
};
})();
// QuerySet type is retained though query chain until terminated.
// Orders are optional, must conform to SortOrder type when present.
// QuerySet.orderBy overloads accept iteratees applicable to QuerySet's type only
const orderByArguments = () => {
// orderByArguments
(() => {
const { Book } = sessionFixture();
const booksQuerySet = Book.all();
// $ExpectType readonly Ref<Book>[]
const singleIteratee = booksQuerySet
booksQuerySet
.orderBy('title')
.orderBy(book => book.publisher, 'desc')
.orderBy(book => book.title, false)
@ -319,7 +321,7 @@ const orderByArguments = () => {
.toRefArray();
// $ExpectType readonly Ref<Book>[]
const arrayIteratee = booksQuerySet
booksQuerySet
.orderBy(['title'], ['asc'])
.orderBy(['publisher', 'title'], [true, 'desc'])
.orderBy([book => book.title], ['desc'])
@ -327,17 +329,18 @@ const orderByArguments = () => {
.orderBy([book => book.title, 'publisher'], ['desc', false])
.toRefArray();
const invalidSingleKeyIteratee = booksQuerySet.orderBy('notABookPropertyKey'); // $ExpectError
const invalidSingleFunctionIteratee = booksQuerySet.orderBy([book => book.notABookPropertyKey], false); // $ExpectError
const invalidStringOrderDirectionType = booksQuerySet.orderBy('title', 'inc'); // $ExpectError
const invalidSingleOrderDirectionType = booksQuerySet.orderBy('title', 4); // $ExpectError
const invalidArrayKeyIteratee = booksQuerySet.orderBy(['notABookPropertyKey']); // $ExpectError
const invalidArrayFunctionIteratee = booksQuerySet.orderBy([book => book.notABookPropertyKey]); // $ExpectError
const invalidArrayStringOrderDirection = booksQuerySet.orderBy(['title'], ['inc']); // $ExpectError
const invalidArrayOrderDirectionType = booksQuerySet.orderBy(['title'], [4]); // $ExpectError
};
booksQuerySet.orderBy('notABookPropertyKey'); // $ExpectError
booksQuerySet.orderBy([book => book.notABookPropertyKey], false); // $ExpectError
booksQuerySet.orderBy('title', 'inc'); // $ExpectError
booksQuerySet.orderBy('title', 4); // $ExpectError
booksQuerySet.orderBy(['notABookPropertyKey']); // $ExpectError
booksQuerySet.orderBy([book => book.notABookPropertyKey]); // $ExpectError
booksQuerySet.orderBy(['title'], ['inc']); // $ExpectError
booksQuerySet.orderBy(['title'], [4]); // $ExpectError
})();
const selectors = () => {
// selectors
(() => {
// test fixture, use reselect.createSelector in production code
const createSelector = <S, OS extends OrmState<any>, Result extends any>(
param1Creator: (state: S) => OS,
@ -363,4 +366,97 @@ const selectors = () => {
);
selector({ db: orm.getEmptyState() }); // $ExpectType Ref<Book>
};
})();
// advanced selectors
(() => {
const orm = ormFixture();
interface RootState {
foo: number;
bar: string;
db: OrmState<Schema>;
}
type TestSelector = (state: RootState) => Ref<Book>;
const selector1 = createOrmSelector(
orm,
s => s.db,
s => s.bar,
(session, title) => session.Book.get({ title })!.ref
) as TestSelector;
const selector2 = createOrmSelector(
orm,
s => s.db,
s => s.foo,
s => s.bar,
(session, id, title) => session.Book.get({ id, title })!.ref
) as TestSelector;
const selector3 = createOrmSelector(
orm,
s => s.db,
s => s.foo,
s => s.bar,
s => s.foo,
(session, id, title, id2) => session.Book.get({ id, title, id2 })!.ref
) as TestSelector;
const selector4 = createOrmSelector(
orm,
s => s.db,
s => s.foo,
s => s.bar,
s => s.foo,
s => s.bar,
(session, id, title, id2, title2) => session.Book.get({ id, title, id2, title2 })!.ref
) as TestSelector;
const selector5 = createOrmSelector(
orm,
s => s.db,
s => s.foo,
s => s.bar,
s => s.foo,
s => s.bar,
s => s.foo,
(session, ...args) => session.Book.get({ title: args[1] })!.ref
) as TestSelector;
const selector6 = createOrmSelector(
orm,
s => s.db,
s => s.foo,
s => s.bar,
s => s.foo,
s => s.bar,
s => s.foo,
s => s.bar,
(session, id, title) => session.Book.get({ title })!.ref
) as TestSelector;
const invalidSelector = createOrmSelector(
orm,
s => s.db,
s => s.foo,
(session, foo, missingArg) => foo // $ExpectError
) as (state: RootState) => number;
const invalidSelector2: TestSelector = createOrmSelector(
orm,
s => s.db,
s => s.foo,
(session, foo) => session.Book.withId(foo)!.ref // $ExpectError
);
const state = { db: orm.getEmptyState(), foo: 1, bar: 'foo' };
selector1(state); // $ExpectType Ref<Book>
selector2(state); // $ExpectType Ref<Book>
selector3(state); // $ExpectType Ref<Book>
selector4(state); // $ExpectType Ref<Book>
selector5(state); // $ExpectType Ref<Book>
selector6(state); // $ExpectType Ref<Book>
})();

View File

@ -1,25 +1,81 @@
import { IndexedModelClasses, ORM, OrmState } from './ORM';
import { OrmSession } from './Session';
export interface ORMReducer<I extends IndexedModelClasses<any>, TAction extends any = any> {
(state: OrmState<I> | undefined, action: TAction): OrmSession<I>;
export interface ORMReducer<I extends IndexedModelClasses, TAction extends any = any> {
(state: OrmState<I> | undefined, action: TAction): OrmState<I>;
}
export type defaultUpdater<I extends IndexedModelClasses<any>, TAction extends any = any> = (
export type defaultUpdater<I extends IndexedModelClasses, TAction extends any = any> = (
session: OrmSession<I>,
action: TAction
) => void;
export function createReducer<I extends IndexedModelClasses<any>, TAction extends any = any>(
export function createReducer<I extends IndexedModelClasses, TAction extends any = any>(
orm: ORM<I>,
updater?: defaultUpdater<I, TAction>
): ORMReducer<I, TAction>;
export interface ORMSelector<I extends IndexedModelClasses<any>, Result extends any = any> {
(session: OrmSession<I>, ...args: any[]): Result;
export type Selector<S, R> = (state: S) => R;
export interface ORMSelector<I extends IndexedModelClasses, Args extends any[], R> {
(session: OrmSession<I>, ...args: Args): R;
}
export function createSelector<I extends IndexedModelClasses<any>, Result extends any = any>(
export function createSelector<S, I, R1, R2, R3, R4, R5, R6, R>(
orm: ORM<I>,
ormSelector: ORMSelector<I, Result>
): (state: OrmState<I>) => Result;
ormStateSelector: Selector<S, OrmState<I>>,
selector1: Selector<S, R1>,
selector2: Selector<S, R2>,
selector3: Selector<S, R3>,
selector4: Selector<S, R4>,
selector5: Selector<S, R5>,
selector6: Selector<S, R6>,
ormSelector: ORMSelector<I, [R1, R2, R3, R4, R5, R6], R>
): Selector<S, R>;
export function createSelector<S, I, R1, R2, R3, R4, R5, R>(
orm: ORM<I>,
ormStateSelector: Selector<S, OrmState<I>>,
selector1: Selector<S, R1>,
selector2: Selector<S, R2>,
selector3: Selector<S, R3>,
selector4: Selector<S, R4>,
selector5: Selector<S, R5>,
ormSelector: ORMSelector<I, [R1, R2, R3, R4, R5], R>
): Selector<S, R>;
export function createSelector<S, I, R1, R2, R3, R4, R>(
orm: ORM<I>,
ormStateSelector: Selector<S, OrmState<I>>,
selector1: Selector<S, R1>,
selector2: Selector<S, R2>,
selector3: Selector<S, R3>,
selector4: Selector<S, R4>,
ormSelector: ORMSelector<I, [R1, R2, R3, R4], R>
): Selector<S, R>;
export function createSelector<S, I, R1, R2, R3, R>(
orm: ORM<I>,
ormStateSelector: Selector<S, OrmState<I>>,
selector1: Selector<S, R1>,
selector2: Selector<S, R2>,
selector3: Selector<S, R3>,
ormSelector: ORMSelector<I, [R1, R2, R3], R>
): Selector<S, R>;
export function createSelector<S, I, R1, R2, R>(
orm: ORM<I>,
ormStateSelector: Selector<S, OrmState<I>>,
selector1: Selector<S, R1>,
selector2: Selector<S, R2>,
ormSelector: ORMSelector<I, [R1, R2], R>
): Selector<S, R>;
export function createSelector<S, I, R1, R>(
orm: ORM<I>,
ormStateSelector: Selector<S, OrmState<I>>,
selector1: Selector<S, R1>,
ormSelector: ORMSelector<I, [R1], R>
): Selector<S, R>;
export function createSelector<I, R>(orm: ORM<I>, ormSelector: ORMSelector<I, [], R>): Selector<OrmState<I>, R>;