How to make two parallel requests with different error types using sequenceT and ReaderTaskEither? - fp-ts

I want to make two parallel requests using sequenceT function and work with results, but it shows me an error which I cannot resolve on my own
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as Ap from 'fp-ts/Apply'
Ap.sequenceT(RTE.ApplyPar)(
getUserById('1'),
getSpecialistById('2')
) // <- This method shows error
Error
Type 'SpecialistNotFoundError' is not assignable to type 'UserNotFoundError'.
Types of property 'code' are incompatible.
Type '"SPECIALIST_NOT_FOUND"' is not assignable to type '"USER_NOT_FOUND"'
Code that may help to understand the problem
function getSpecialistById (specialistId: string): RTE.ReaderTaskEither<PrismaClient, SpecialistNotFoundError, Specialist> {
return (prisma: PrismaClient) => {
return TE.tryCatch(
() => prisma.specialist.findUnique({ where: { id: specialistId }, rejectOnNotFound: true }),
() => new SpecialistNotFoundError()
)
}
}
function getUserById (userId: string): RTE.ReaderTaskEither<PrismaClient, UserNotFoundError, User> {
return (prisma: PrismaClient) => {
return TE.tryCatch(
() => prisma.user.findUnique({ where: { id: userId }, rejectOnNotFound: true }),
() => new UserNotFoundError()
)
}
}
class UserNotFoundError extends Error {
readonly code = 'USER_NOT_FOUND'
constructor () {
super('User not found')
}
}
class SpecialistNotFoundError extends Error {
readonly code = 'SPECIALIST_NOT_FOUND'
constructor () {
super('Specialist not found')
}
}

I think rather than using sequenceT (as I don't think it's capable of handling the types correctly for what you're trying to do) I would instead use Do notation like follows:
const result = pipe(
RTE.Do,
RTE.apS("user", getUserById("1")),
RTE.apSW("spec", getSpecialistById("2")),
RTE.map(({ user, spec }) => {
return `${user.id}:${spec.id}`;
})
);
result will be RTE.ReadTaskEither<PrismaClient, UserNotFoundError | SpecialistNotFoundError, string> in this case since I returned a string in map.
Here are the docs on Do notation. Note at the bottom where it shows how to do things in parallel

Related

TypeScript extend parameter type

I want to call a function from my UI library with a type which extends the original (Suggestion) parameter type with additional properties. According to this link it looks like it should be possible: https://github.com/Microsoft/TypeScript/issues/2225 (bottom comments)
What am I doing wrong and do you do this correctly? Note that the getSuggestion function and its type is located in a UI library package and I want to call it from my apps package.
Playground link
Code:
import React from 'react'
type Suggestion = {
name: string
}
interface UIProps {
getSuggestion: <T = {}>(suggestion: Suggestion & T) => string
}
function UIComponent(props: UIProps) {
return null
}
type AppSuggestion = {
name: string
enabled: boolean
}
function AppComponent() {
return (
<UIComponent
getSuggestion={(suggestion: AppSuggestion) => 'hello from app'}
/>
)
}
Error:
I have also tried using interfaces instead, but without luck.
Playground link
Code:
import React from 'react'
interface Suggestion {
name: string
}
interface UIProps {
getSuggestion: <T>(suggestion: T) => string
}
function UIComponent(props: UIProps) {
return null
}
interface AppSuggestion extends Suggestion {
name: string
enabled: boolean
}
function AppComponent() {
return (
<UIComponent
getSuggestion={(suggestion: AppSuggestion) => 'hello from app'}
/>
)
}
Error:
The generic type parameter should be on the interface and the component, not the function. If you have a generic function the implementation ((suggestion: AppSuggestion) => 'hello from app') needs to accept any type argument for the type parameter, which it currently does not.
You need to put the type parameter on the component and the props. This means that the type parameter will be inferred when you create your component:
interface UIProps<T> {
getSuggestion: (suggestion: T) => string
}
function UIComponent<T>(props: UIProps<T>) {
return null
}
interface AppSuggestion extends Suggestion {
name: string
enabled: boolean
}
function AppComponent() {
return (
<UIComponent
getSuggestion={(suggestion: AppSuggestion) => 'hello from app'}
/>
)
}
Playground Link

React Query - Combine properties from multiple custom hooks that use useQuery with type safety

I have an arbitrary amount of custom hooks that return the query result from useQuery. What I'm trying to do is combine the return values from those hooks into one object with this structure:
{
data,
isLoading,
isFetching,
isSuccess,
}
The data property will be an array created from combining the data properties on all the results.
This is my current code:
export function isAnyProperty(
property: keyof QueryObserverBaseResult<unknown, unknown>,
...queries: UseQueryResult<unknown, unknown>[]
) {
return queries.some((query) => query[property]);
}
export function combineQueryResults<TData extends unknown[], TQueries extends UseQueryResult<TData>[]>(
...queries: TQueries
) {
const data = queries.reduce<TData[]>((acc, query) => {
if (query.data === undefined) {
return acc;
}
return [...acc, query.data];
}, []);
return {
data,
isLoading: isAnyProperty('isLoading', ...queries),
isFetching: isAnyProperty('isFetching', ...queries),
isSuccess: isAnyProperty('isSuccess', ...queries),
};
}
I can't get TypeScript to infer the values for TData when I am using rest parameters, and I am not quite sure why. One thing to note is that I did manage to get TypeScript to infer the correct types when data was an array of UseQueryResult, just not one level down with TData.
For reference, what I get back currently from data is unknown[][]. What I am expecting to get back is of course a tuple with the data values from UseQueryResult. I thought maybe the reduce function removing all undefined values might mess with TypeScript being able to infer the correct values - so I tried it with a simple map as well, which results in data: (unknown[] | undefined)[] - so that didn't work either.
I also tried this:
export function combineQueryResults<TData extends unknown[]>(...queries: UseQueryResult<TData>[]) {
const data = queries.map((query) => query.data);
return {
data,
isLoading: isAnyProperty('isLoading', ...queries),
isFetching: isAnyProperty('isFetching', ...queries),
isSuccess: isAnyProperty('isSuccess', ...queries),
};
}
But with this code, typescript will only infer the type for the first variable passed to combineQueryResult and results in a Type 'TypeOne[]' is not assignable to type 'TypeTwo[]' error.
Looks like typescript will infer the type TData when used with UseQueryResult, but not on its own:
unknown[], Vod.ConstructedAsset[], and Group.Union[] are all TData.
I created this function to combine my queries:
const combineResults = <T extends object>(
data: UseQueryResult<T[], unknown>[]
): T[] =>
data.reduce((acc, curr) => {
if (!curr.data) return acc;
return [...acc, ...curr.data];
}, []);
//my hook
const useMyHook = (teams: TeamInfo[]) => {
const result = useQueries(
//Your queries here...
);
return {
data: combineResults<MyInterface>(result),
isLoading: result.some((query) => query.isLoading),
isFetching: result.some((query) => query.isFetching),
};
};
I hope this helps you.

How to conditionally call different optional function props in a React TypeScript component without getting compiler error?

I have a React TypeScript component which accepts multiple props of which some are optional and these include functions as well. Below is a rough example of what my actual code looks like
interface AnimalProps {
animalName: string;
bark?: () => void;
swim?: () => void;
}
const Animal = ({ animalName, bark, swim }: AnimalProps) => {
/* some rendering here */
}
I also call the bark or swim function depending upon the animalName.
The issue is since both of these are optional props, typescript keeps throwing the error
Cannot invoke an object which is possibly undefined. How can I assert to the compiler that at least one of these function will definitely be passed depending upon the animalName ?
Apart from the suggested solutions, you may also use discriminating unions: link to the doc which is designed specifically to solve this kind of situations (refer the doc).
like this:
type DogProps = {
animalName: "dog";
bark: () => void;
}
type FishProps = {
animalName: "fish";
swim: () => void;
}
type AnimalProps = DogProps | FishProps;
const Animal = (props: AnimalProps) => {
if(props.animalName === 'dog') {
props.bark();
} else {
props.swim();
}
}
TS Playground link: https://tsplay.dev/NddryN
Destructing isn't possible until you narrow down the type like this (you see other props resolve appropriately without hassle of checking):
type DogProps = {
animalName: "dog";
bark: () => void;
dogProp: string;
}
type FishProps = {
animalName: "fish";
swim: () => void;
fishProp: number;
}
type AnimalProps = DogProps | FishProps;
const Animal1 = (props: AnimalProps) => {
if(props.animalName === 'dog') {
const { bark, dogProp } = props;
bark();
} else {
const {swim, fishProp} = props;
swim();
}
}
see this: https://tsplay.dev/N9pn7w
You can simply check if the function exists before calling it.
if (animalName === 'dog' && typeof bark === 'function') {
bark();
}
If you feel like that is too verbose, you can also take advantage of optional-chaining to do the same thing.
if (animalName === 'dog') {
bark?.(); // Will only invoke bark() if it's defined
}
Optional-chaining is supported in TypeScript since version 3.7.

Typescript adding "| null" to return type of Promise.all

I have an async TS function that makes a request and casts the response data to a boolean and returns it, but in the calling function VS Code is telling me the return value is boolean | null when I make the call in Promise.all. Here's the code:
The function:
import apiAxios from "../apiAxios";
export default async function doesAssignmentHaveTakes(
assignmentId: number
): Promise<boolean> {
const response = await apiAxios.get(`/assignments/${assignmentId}/has-takes`);
return !!response.data;
}
And the caller:
import React, { FC, useState, useCallback } from "react";
import styled from "styled-components/macro";
import AssignmentForm, {
Props as AssignmentFormProps,
Value as AssignmentFormValue
} from "./AssignmentForm";
import useAsyncEffect from "../utils/useAsyncEffect";
import getAssignmentById from "../api/assignments/getAssignmentById";
import doesAssignmentHaveTakes from "../api/assignmentTakes/doesAssignmentHaveTakes";
interface Props extends AssignmentFormProps {
assignmentId: number;
onSubmit(value: Value): any;
}
export interface Value extends AssignmentFormValue {
assignmentId: number;
}
const EditAssignmentForm: FC<Props> = props => {
const { assignmentId, onSubmit, ...rest } = props;
const [showEditWarning, setShowEditWarning] = useState(false);
const [initialValue, setInitialValue] = useState<AssignmentFormValue | null>(
null
);
useAsyncEffect(
async isCancelled => {
const [fetchedAssignment, hasTakes] = await Promise.all([
getAssignmentById(assignmentId),
doesAssignmentHaveTakes(assignmentId)
]);
if (!fetchedAssignment) {
// TODO: Alert parent component?
return;
}
const value: Value = {
assignmentId: fetchedAssignment.id,
assignment: {
title: fetchedAssignment.title,
subTitle: fetchedAssignment.subTitle
},
sets: fetchedAssignment.sets
.map(set => ({
id: set.id,
index: set.index,
questions: set.questions
.map(question => ({
id: question.id,
index: question.index,
isPractice: question.isPractice,
questionText: question.questionText,
inputType: question.inputType,
questionImage: question.questionImage,
sampleResponseText: question.sampleResponseText,
sampleResponseImage: question.sampleResponseImage
}))
.sort((a, b) => a.index - b.index),
learningTarget: set.learningTarget,
isExampleCorrect: set.isExampleCorrect,
exampleImage: set.exampleImage,
character: set.character
}))
.sort((a, b) => a.index - b.index)
};
if (!isCancelled()) {
setInitialValue(value);
setShowEditWarning(hasTakes);
}
},
[assignmentId]
);
const handleSubmit = useCallback(
(value: AssignmentFormValue) => {
onSubmit({
...value,
assignmentId
});
},
[onSubmit, assignmentId]
);
if (!initialValue) {
// Loading...
return null;
}
return (
<AssignmentForm
{...rest}
initialValue={initialValue}
onSubmit={handleSubmit}
/>
);
};
export default styled(EditAssignmentForm)``;
The specific lines with the issue:
const [fetchedAssignment, hasTakes] = await Promise.all([
getAssignmentById(assignmentId),
doesAssignmentHaveTakes(assignmentId)
]);
And
setShowEditWarning(hasTakes);
The TS error:
TypeScript error in /Users/james/projects/math-by-example/client/src/components/EditAssignmentForm.tsx(71,28):
Argument of type 'boolean | null' is not assignable to parameter of type 'SetStateAction<boolean>'.
Type 'null' is not assignable to type 'SetStateAction<boolean>'. TS2345
69 | if (!isCancelled()) {
70 | setInitialValue(value);
> 71 | setShowEditWarning(hasTakes);
| ^
72 | }
73 | },
74 | [assignmentId]
And some screenshots of the error in VS Code
Why does TS add null to the resolved types of Promise.all?
The solution is to add as const to the array you pass to Promise.all.
Explanation
The problem is not with the typing of Promise.all or a bug in the compiler. The issue is what TypeScript does by default with an array. Consider this:
const q = [1, "a"];
The default type inference for q will be (string | number)[]. Even though you have a number as the first position, and a string as the second, TypeScript infers that all positions can be either a string or a number. If you want TypeScript to treat the array as a tuple and assign to each position the narrowest type possible, you can do:
const q = [1, "a"] as const;
TS will infer a type of readonly [1, "a"] for this array. So q can have only the number 1 in the first position and the string "a" in the second. (It is also readonly but that's a side issue here.) This was introduced in TypeScript 3.4.
Ok, what does this have to do with your case? When you pass your array to Promise.all TypeScript is using the kind of type inference I've shown in my first example. Promise.all sees an array in which each item can take the union of all values that the items can take. If you use as const then the inference will be like the second case I've shown above, and this will be reflected accordingly in the type that Promise.all gets. Again, there's no problem with Promise.all's typing. It works with what it gets: bad typing in, bad typing out.
Here's an illustration (also in the playground):
async function fa(): Promise<string | null> { return "foo"; }
async function fb(): Promise<boolean> { return true; }
async function main(): Promise<void> {
let a: string | null;
let b: boolean;
// Remove this "as const" and the code won't compile.
[a, b] = await Promise.all([fa(), fb()] as const);
console.log(a, b);
}
main();
This has been resolved from ts 3.9+ (release note), upgrade to 3.9 and you will not see this error.
The problem is with the type definitions for Promise.all, take a look. The return type of the .all always try to make a union of the array promises generic types.
Probably your other function getAssignmentById can return null, so the Promise.all will infer a return type of [something | null, boolean | null]. It is possibly a bug with the TS compiler, I am not sure. I made a playground with the same conditions to see the inference in practice, take a look at how it infers the generic types on Promise constructor, then remove the null from the return type of funcB and see the Promise.all type again... it behaves as expected.
Agree with Pedro's answer that Promise.all doesn't deal with different return types out of the box.
You can try declaring the return type of your Promise like this:
const [fetchedAssignment, hasTakes] = await Promise.all<string | null, boolean>([
getAssignmentById(assignmentId),
doesAssignmentHaveTakes(assignmentId)
]);

How to write type definitions for React HoCs

I have a higher order component that deals with Firestore data for me. I'm pretty new to typescript and I'm having trouble getting the types to work as I'd like them to.
Here are the full files + some extra ts definitions
I have a couple problems:
React.Component not inferring type definitions:
type WithFirestoreHoC<Props = {}> = (
config: WithFirestoreConfig<Props>,
) => (
WrappedComponent: ComponentType<WithFirestore & Props>,
) => ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>;
const withFirestore: WithFirestoreHoC = ({
queries,
props: propPickList,
loading: { delay = 200, timeout = 0 } = {},
}) => WrappedComponent =>
class WithFirestoreConnect extends Component { ... }
config and WrappedComponent are getting their type definitions (as WithFirestoreConfig + ComponentType<WithFirestore & Props>, respectively.
However, WithFirestoreConnect is not inferring that it should be ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>.
I wouldn't mind defining the state twice, but that doesn't help me with getting Props from type WithFirestoreHoC<Props = {}> to class WithFirestoreConnect extends Component<Props, { error: Error; queries: {}; loaded: boolean }> { ... } because it can't find Props.
How to create a dynamic pick list
Part of WithFirestoreConfig defines that the config object has a list of props that get passed on to WrappedComponent
WrappedComponent: ComponentType<WithFirestore & Props>,
should really be
WrappedComponent: ComponentType<WithFirestore & Pick<Props, config.propsPickList>,
Is there a way to tell typescript that what you provide in config.propsPickList will determine what props WrappedComponent should expect?
Inferring Firestore types
There are 2 types of Firestore query responses, those for Documents and those for Collections/Queries. It would be amazing if those could be defined in config.queries as something like this:
{ queries: {
docQuery: myDocument as DocumentReference<docDataType>,
collectionQuery: myDocument as CollectionReference<docDataType>,
} }
so WrappedComponent could know whether to expect a query or document data structure on the other end.
This seems super complex so I have a simpler example (it's a shortcut that creates a single subscription) here that would at least be a good stepping stone towards what I want:
export const withFirestoreDocument: <
DataType = firestore.DocumentData,
Props = {}
>(
query: FirestoreQueryable<DataType>,
) => (
WrappedComponent: ComponentType<DocumentSnapshotExpanded<DataType>>,
) => WithFirestoreHoC<Props> = query => WrappedComponent =>
withFirestore({ queries: { _default: query } })(
mapProps<
DocumentSnapshotExpanded<DataType> & Props,
{ _default: DocumentSnapshotExpanded<DataType> } & Props
>(({ _default, ...props }) => ({ ...props, ..._default }))(WrappedComponent),
);
However I'm stuck here because I can't get mapProp's type definitions to pull from the function's type defs... What's the right way to do this?
React.Component not inferring type definitions: Make Props a type parameter of the function instead of the type alias and then declare it when you define withFirestore.
How to create a dynamic pick list: Add a PL type parameter for the union of the elements of the pick list. This will do the right thing when you let TypeScript infer PL at a call site, though it's possible for callers to produce unsound behavior by specifying PL to be a union type including elements that are not in the actual list.
Inferring Firestore types: I'm not sure where you were going with withFirestoreDocument. You can do this with another Q type parameter and some mapped types and conditional types to generate the types of the injected props from Q.
Here is my revision of withFirestore.tsx with all the new features, some unrelated fixes to get it to compile in my environment, and an example added at the bottom (which should probably rather be in a separate file):
import * as React from 'react';
import { Component, ComponentClass, ComponentType } from 'react';
import {
DocumentReference,
Query,
CollectionReference,
DocumentSnapshotExpanded,
QuerySnapshotExpanded
} from './firemodel';
import { firestore } from 'firebase';
import { pick, forEach, isEqual, isFunction } from 'lodash';
import { expandDocSnapshot, expandQuerySnapshot } from 'modules/providers/util';
import SmartLoader from 'modules/atoms/SmartLoader';
type FirestoreQueryable<DataType> =
| DocumentReference<DataType>
| Query<DataType>
| CollectionReference<DataType>;
type FirestoryQueryableFunction<
DataType,
Props
> = (
firestore: firestore.Firestore,
props: Props,
) => Promise<FirestoreQueryable<DataType>>;
type QueryConfigEntry<Props> =
FirestoreQueryable<any> | FirestoryQueryableFunction<any, Props>;
type QueryConfig<Props> = {
[queryName: string]: QueryConfigEntry<Props>;
};
type FirestoreQueryableExpanded<Props, QE extends QueryConfigEntry<Props>> =
QE extends FirestoreQueryable<any> ? FirestoreQueryableExpanded1<QE> :
QE extends FirestoryQueryableFunction<any, Props> ? FirestoreQueryableExpanded1<ReturnType<QE>> : unknown;
type FirestoreQueryableExpanded1<QE extends FirestoreQueryable<any>> =
QE extends CollectionReference<infer DataType> | Query<infer DataType> ? QuerySnapshotExpanded<DataType> :
QE extends DocumentReference<infer DataType> ? DocumentSnapshotExpanded<DataType> : unknown;
interface WithFirestoreConfig<Props, PL extends keyof Props, Q extends QueryConfig<Props>> {
/** Object containing the queries to be provided to WrappedComponent.
* The queryName used is also the prop name the snapshot is passed in. */
queries: Q;
/** A list of props to whitelist passing to WrappedComponent.
* Configs without a list will whitelist all props */
props?: PL[];
/** Loading config items */
loading?: {
/** Number of ms after which to display the loading icon */
delay?: number;
/** Number of ms after which to display the timeout message */
timeout?: number;
};
}
type WithFirestoreHoC = <Props>() => <PL extends keyof Props, Q extends QueryConfig<Props>>(
config: WithFirestoreConfig<Props, PL, Q>,
) => (
WrappedComponent: ComponentType<WithFirestore<Props, Q> & Pick<Props, PL>>,
) => ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>;
const withFirestore: WithFirestoreHoC =
// An extra function call is needed so that callers can specify Props and
// still have PL and Q inferred. It can be removed when
// https://github.com/Microsoft/TypeScript/issues/10571 is implemented.
<Props extends {}>() =>
// Note: if `props` is not passed, there will be no inference for PL and it
// will default to its constraint, which is exactly the behavior we want as
// far as typing is concerned.
<PL extends keyof Props, Q extends QueryConfig<Props>>({
queries,
props: propPickList,
loading: { delay = 200, timeout = 0 } = {},
}: WithFirestoreConfig<Props, PL, Q>) => WrappedComponent =>
class WithFirestoreConnect extends Component<Props, { error: Error; queries: WithFirestore<Props, Q>; loaded: boolean }> {
subscriptions: {
[queryName: string]: ReturnType<FirestoreQueryable<any>['onSnapshot']>;
} = {};
state = {
error: null as Error,
queries: {} as WithFirestore<Props, Q>,
loaded: false,
};
componentDidMount() {
this.restartSubscription();
}
cancelSubscriptions = () => {
forEach(this.subscriptions, unsubscribe => unsubscribe());
this.subscriptions = {};
};
restartSubscription = () => {
// Open questions:
// - figuring out when all loaded (use a promise?)
this.cancelSubscriptions();
forEach(queries, async (q: QueryConfigEntry<Props>, key) => {
let ref: FirestoreQueryable<any>;
if (isFunction(q)) {
// The fact that this is an async/await means that we can
// create dependent queries within our FirestoreQueryableFunction
ref = await q(firestore(), this.props);
} else {
// Narrowing is not working for some reason.
ref = q as FirestoreQueryable<any>;
}
if (ref instanceof firestore.DocumentReference) {
this.subscriptions[key] = ref.onSnapshot(
snap => {
this.setState({
queries: Object.assign({}, this.state.queries, {[key]: expandDocSnapshot(snap)}),
});
},
err => {
console.error(JSON.stringify(err));
this.setState({ error: err });
this.cancelSubscriptions();
},
);
} else if (
ref instanceof firestore.CollectionReference ||
ref instanceof firestore.Query
) {
let ref2: {onSnapshot(os: (snap: firestore.QuerySnapshot) => void, oe: (err: Error) => void): () => void; } = ref;
this.subscriptions[key] = ref2.onSnapshot(
snap => {
this.setState({
queries: Object.assign({}, this.state.queries, {[key]: expandQuerySnapshot(snap)}),
});
},
err => {
console.error(JSON.stringify(err));
this.setState({ error: err });
this.cancelSubscriptions();
},
);
}
});
};
componentDidUpdate(prevProps: Props) {
if (!isEqual(this.props, prevProps)) {
this.restartSubscription();
}
}
componentWillUnmount() {
this.cancelSubscriptions();
}
render() {
if (!this.state.loaded || this.state.error) {
return (
<SmartLoader
error={this.state.error}
timeout={timeout}
delay={delay}
/>
);
}
const whitelistedProps = propPickList
? pick(this.props, propPickList)
: this.props;
// Unsure what's wrong here ~ Matt
let WrappedComponent2 = WrappedComponent as any;
return <WrappedComponent2 {...whitelistedProps} {...this.state.queries} />;
}
};
export type WithFirestore<Props, Q extends QueryConfig<Props>> = {
[queryName in keyof Q]: FirestoreQueryableExpanded<Props, Q[queryName]>;
}
export default withFirestore;
// EXAMPLE
interface MyDoc {
y: number
}
declare let myDocRef: DocumentReference<MyDoc>;
declare let myCollRef: CollectionReference<MyDoc>;
let wrapped = withFirestore<{x: string}>()({
queries: {
myDoc: myDocRef,
myColl: myCollRef
},
})((props) => { return <>{props.myDoc.data.y + props.myColl.docs[props.x].data.y}</>; });

Resources