ClientQuerier and UniversalQuerier declare the same 13 methods twice. The only differences are the return wrapper (RequestSuccessResponse<T> / RequestCountedSuccessResponse<T>) and the options type (RequestOptions instead of QueryOptions). browser/type/clientQuerier.ts says why:
These differences prevent clean extends UniversalQuerier - TypeScript does not support higher-kinded type wrappers, so the interfaces are kept in sync by convention.
That is beatable by defunctionalizing the wrapper: put the shapes in a lookup keyed by transport, and select with an indexed access.
Why now
The find methods carry five type parameters each since QueryFindResult landed, so every read signature is ~7 lines rather than 1, duplicated across both interfaces. Sync-by-convention is a worse deal at that size, and drift is silent in the direction that matters: a client signature that falls behind stops narrowing rows without failing anything.
The mechanism (prototyped, works)
interface ReadResult<T> {
server: { one: Promise<T | undefined>; many: Promise<T[]>; counted: Promise<[T[], number]> };
client: {
one: Promise<RequestSuccessResponse<T | undefined>>;
many: Promise<RequestSuccessResponse<T[]>>;
counted: Promise<RequestCountedSuccessResponse<T[]>>;
};
}
type Transport = keyof ReadResult<unknown>;
type Read<W extends Transport, Shape extends keyof ReadResult<unknown>[Transport], T> = ReadResult<T>[W][Shape];
interface Reads<W extends Transport, O> {
findMany<E extends object, const S extends FieldKey<E> = never /* … */>(
entity: Type<E>,
q: QueryProjected<E, S, V, X, P>,
opts?: O,
): Read<W, 'many', QueryFindResult<E, S, V, X, P>>;
}
interface UniversalQuerier extends Reads<'server', QueryOptions> {}
interface ClientQuerier extends Reads<'client', RequestOptions> {}
Checked on a prototype: the projection still narrows through it on both transports, a typo'd key is still reported, and return types resolve to a plain Promise<Story[]> in errors and hovers - the Read<…> indirection does not leak into diagnostics.
Scope
Blind alleys, so nobody retries them
- Sharing the signatures with the implementing classes (
AbstractQuerier, AbstractQuerierPool, HttpQuerier) is not possible. A class merged with an interface that restates one of its own members is TS2300: Duplicate identifier; a class-interface merge can only add members. Arrow-function properties would work and are not worth per-instance closures on a read path.
- Letting
Querier inherit the entity-argument overload and declare only the $entity one is not possible either. A derived interface member replaces the base member instead of merging with it, and must be assignable to it: declaring only the $entity form gives TS2430: Interface 'Querier' incorrectly extends 'UniversalQuerier' and breaks findMany(Entity, {…}) everywhere.
ClientQuerierandUniversalQuerierdeclare the same 13 methods twice. The only differences are the return wrapper (RequestSuccessResponse<T>/RequestCountedSuccessResponse<T>) and the options type (RequestOptionsinstead ofQueryOptions).browser/type/clientQuerier.tssays why:That is beatable by defunctionalizing the wrapper: put the shapes in a lookup keyed by transport, and select with an indexed access.
Why now
The find methods carry five type parameters each since
QueryFindResultlanded, so every read signature is ~7 lines rather than 1, duplicated across both interfaces. Sync-by-convention is a worse deal at that size, and drift is silent in the direction that matters: a client signature that falls behind stops narrowing rows without failing anything.The mechanism (prototyped, works)
Checked on a prototype: the projection still narrows through it on both transports, a typo'd key is still reported, and return types resolve to a plain
Promise<Story[]>in errors and hovers - theRead<…>indirection does not leak into diagnostics.Scope
findOneById,findOne,findMany,findManyAndCountshared;findManyStreamstays server-onlyinsertOne/insertMany/update*/upsert*/save*/delete*/restore*, where the duplication is the other ~90 linesdeleteOneByIdtakesQueryOptions & RequestOptionson the client) carried as a parameter rather than special-casedClientQuerierCoversAllCrudOperationskeeps passing (keyofincludes inherited members, so it should)Blind alleys, so nobody retries them
AbstractQuerier,AbstractQuerierPool,HttpQuerier) is not possible. A class merged with an interface that restates one of its own members isTS2300: Duplicate identifier; a class-interface merge can only add members. Arrow-function properties would work and are not worth per-instance closures on a read path.Querierinherit the entity-argument overload and declare only the$entityone is not possible either. A derived interface member replaces the base member instead of merging with it, and must be assignable to it: declaring only the$entityform givesTS2430: Interface 'Querier' incorrectly extends 'UniversalQuerier'and breaksfindMany(Entity, {…})everywhere.