How to require interface members for response data? - reactjs

In the code below (it compiles just fine), it was my expectation that any response data that did not conform to the interface IStory, would throw an error. However, this is not the case, and all the data is put in state normally. But there is certainly no thisPropertyDoesNotExist property on any of the response objects. Why isn't the interface being required here? How would that standard be implemented?
The end goal is for the response data to have at least the data defined in the interface (I'm really interested in checking if url's are not present).
import React, { useState, useEffect } from "react";
import { getStory } from "../services/hnAPI";
interface Props {
storyId: number;
}
interface IStory {
id: number;
title: string;
url: string;
thisPropertyDoesNotExist: string;
}
export const Story: React.FC<Props> = (props) => {
const [story, setStory] = useState<IStory | null>(null);
useEffect(() => {
getStory(props.storyId)
.then((data: Required<IStory>) => {
data && setStory(data);
})
.catch((error) => {
console.log(error);
setStory(null);
});
}, [props.storyId]);
return story ? (
<p>
<a href={story.url}>{story.title}</a>
</p>
) : null;
};

The end goal is for the response data to have at least the data defined in the interface (I'm really interested in checking if url's are not present).
Interfaces for json objects are just your promises (not validated at runtime) that the data will match what you say it will. TypeScript does not exist at runtime (its just JavaScript) and you cannot use interfaces are runtime.
Solution
You can however write JavaScript validation code that
* Returns a type to TypeScript
* Ensures the object matches the type
An example of such a library is: https://github.com/pelotom/runtypes

Related

Type error for api function in react-query

import {useMutation, useQueryClient} from 'react-query';
import axios from 'axios';
interface userInterface {
email: string;
password: string;
}
const loginUser = (user:userInterface) => {
return axios.post('http://127.0.0.0/login',user);
};
export const useLoginApi = () => {
interface BodyInterface {
email: string;
password: string;
}
interface ErrorInterface {
status: string;
}
const {
mutate: loginUserMutator,
isError,
error,
} = useMutation<BodyInterface, ErrorInterface>(loginUser);
return {
loginUserMutator,
isError,
error,
};
};
In here loginUser function showing as an error saying,
No overload matches this call.The last overload gave the following error.Argument of type '() => Promise<AxiosResponse<any, any>>' is not assignable to parameter of type 'MutationKey'.ts(2769).useMutation.d.ts(6, 25): The last overload is declared here.
useMutation has 4 generics. If you only provide two, the other 2 will fall-back to the default generic. The 3rd one is for the variables, that explains the error. The first generic is for the return type of the mutation. You're not showing that it returns, and BodyInterface and userInterface are pretty much the same type ...
What you'd want is at least:
useMutation<User, ErrorInterface, BodyInterface >(loginUser);
User - being what loginUser returns. Not sure what that is ...
BodyInterface - being what you pass as variables to the mutation
but then again, the preferred way is to type the loginUser function and let the rest be inferred:
const loginUser = (user:userInterface): Promise<User> => {
return axios.post('http://127.0.0.0/login',user);
};
useMutation(loginUser);
this will automatically infer input and output depending on loginUser. No, you cannot infer the Error type because there is no guarantee that this is really the error being returned, and rejected Promises cannot be typed. If you really want that, you need to pass the generics as shown above.

Obtaining typescript type of trpc or react-query's useQuery result "data" property

In my React + TRPC application, I call const { data: promotion } = trpc.promotions.getPromotion.useQuery({ id: promotionId }) in the client-side. promotion's type is detected by looking at the return type of the actual server-side code:
getPromotion: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async({ ctx, input }) => {
const promotion = await ctx.prisma.promotion.findUniqueOrThrow({
where: {
id: input.id
}
})
return {
...promotion,
canModify: false
}
})
However, I want to use this same type in an interface another component:
interface Example {
promotion: TheDetectedTypeOfGetPromotionReturn??
}
The problem is, I can't find a way to "extract" the type I need.
I thought maybe I could use ReturnType<typeof trpc.promotions.getPromotion.useQuery>['data'] but that just returns unknown. Yet, data is a valid type on the returned object.
Here's where the useQuery code is defined: https://github.com/trpc/trpc/blob/9e84d4d9c43de3272aa84bf4110e31e47dc5dda6/packages/react-query/src/shared/hooks/createHooksInternal.tsx#L427
Here's the query base type it uses from react-query: https://github.com/TanStack/query/blob/1614c31f84744c62b5bee8f2ff97fc15a1652e86/packages/query-core/src/types.ts#L367
I managed to do it like this
import {type UseTRPCQueryResult} from '#trpc/react query/shared'
import {type inferRouterOutputs} from '#trpc/server'
import {type TRPCClientErrorLike} from '#trpc/react-query'
type YourType = UseTRPCQueryResult<inferRouterOutputs<AppRouter>['promotions']['getPromotions'], TRPCClientErrorLike<AppRouter>>['data']

Simplify boilerplate code for redux actions with typescript

Whenever we need to add a new action to our react/redux application, we copy a lot of boilerplate code, I'd like a helper to try and speed up this process, whilst this is a simple example, it's basically all we'll need to automate.
We create an Action interface, Payload interface, the action payload will need to be exported somehow, and the "action" function as well.
interface Payload {
something: {
[key: string]: boolean;
}
}
export interface Action {
type: 'UniqueRequestKey';
payload: Payload
}
export const someAction = (payload: Payload): Action => ({
type: 'UniqueRequestKey',
payload
});
The above is what we're currently doing, but i feel like a helper method could make this a lot simpler as it's a lot of repetitive code, we're doing this over and over and over again.
I'd like something simple like the following, doesn't have to be exact at all!
const [someAction, Action??] = createAction<'UniqueRequestKey', {
something: {
[key: string]: boolean;
}
}>();
I know the above isn't correct, but if someone could steer me in the right direction that would be great, I've attempted this with the following but it clearly is wrong
type Combine<T, P> = {
type: T,
payload: P
};
function createAction<T,P>(): [(payload: P):Combine<T,P> => {}, type: T] {
return [(payload: P): Combine<T,P> => ({
type,
payload
}), type];
}
Something like
const [someAction, Action] = createAction<'UniqueRequestKey', {
something: {
[key: string]: boolean;
}
}>();
cannot work, because types aren't something that can be created at runtime (they are only compile-time concepts).
But, you can get the type definition from them.
Imagine a simple helper function like this:
function createAction<Type extends string, Payload>(type: Type, payload: Payload) {
return { type, payload }
}
which can be used to create your own app's actions:
export const someAction = (payload: {
something: {
[key: string]: boolean;
}
}) => createAction('UniqueRequestKey', payload)
which you can extract the type with
type SomeActionType = ReturnType<typeof someAction>
which evaluate to
{
type: "UniqueRequestKey";
payload: {
something: {
[key: string]: boolean;
};
};
}
Now, if you do have a lot of actions, you may want to consider to just put them all in one big object,
const actions = {
someAction: (payload: {
something: {
[key: string]: boolean;
}
}) => createAction('UniqueRequestKey', payload),
someOtherAction: (payload: {
fooBar: number
}) => createAction('BlaBla', payload),
}
which you can use to extract all the types like so:
type ActionsCollection = typeof actions
type Actions = ReturnType<
ActionsCollection[keyof ActionsCollection]
>
where Actions is a big union with all the available actions you have in the application. From there, if you need one particular action, you could just extract it from it
type SomeActionType = Extract<Action, { type: 'UniqueRequestKey' }>
Generally, you should not write something like that yourself, but use the official Redux Toolkit, which not only contains a createAction function, but also a createSlice function which will further reduce your code - and especially your TypeScript usage by a lot.
Generally, modern Redux does not use switch..case reducers, ACTION_TYPES are an implementation details, action creators are autogenerated, you can use mutable reducer logic (in createSlice) and write almost no TypeScript. All in all it's probably 1/4 of the code.
Please follow the official Redux Tutorial to learn current Redux with Redux Toolkit.

Object is possibly 'undefined'. TS2532

I keep getting Object is possibly 'undefined'. TS2532 and I know that it's due to assignment of setViewer to undefined, which I have done for the purpose of login matching but I came across with another problem. Right now, I have 2 data coming in homeviewer and one of them is announcements and when I try to access the data, it gives me TS2532 error. Here are the code below. I am new to these and I don't know what to change for the setViewer (setViewer: () => undefined) in order it for to work.
homeViewer.ts
import { createContext } from 'react';
import { HomeViewer } from '../types/home_viewer';
export interface ViewerContextType {
viewer?: HomeViewer;
setViewer: (home: HomeViewer) => void;
}
export const ViewerContext = createContext<ViewerContextType>({
viewer: {},
setViewer: () => undefined,
});
That takes interface from
export interface HomeViewer {
user?: User;
announcements?: Announcement;
}
The part where I use it
const { viewer } = useContext(ViewerContext);
const data = viewer?.announcements;
console.log(data[0]);
If I understand correctly, your issue is revolving around accessing elements at specific indexes from data – where data possibly has a value of undefined (as per your HomeViewer interface);
export interface HomeViewer {
user?: User;
announcements?: Announcement;
}
To me, it appears that the Announcement type is indicative of an array of some elements (judging by the way you're accessing its data), or undefined (since it's optional).
Meaning, it will never certainly be an instance of Announcement's type declaration, so you'll have to chain an optional access operator for accessing array elements that possibly don't exist: ?.;
const { viewer } = useContext(ViewerContext);
const data = viewer?.announcements;
console.log(data?.[0]) // -> Ok!
console.log(data[0]) // -> Err! Object is possibly undefined.
It could be because if viewer is undefined then data will also be undefined, and you can't do data[0] because is the same as undefined[0]

No index signature with a parameter of type 'string' was found on type '[] | iCommits'.ts(7053)

I'm having a fair amount of issues with this file migration to typescript, so overall would appreciate any feedback on what could be improved to make this more type safe and less redundant possibly. This is the code block I am working with:
import React, { useEffect, useState } from 'react'
import axios from 'axios'
interface iProps {
repoName: string
}
interface iCommits {
[index: string]: iCommit[],
}
interface iCommit {
sha: string,
commit: iCommitData
}
interface iCommitData {
author: iAuthor
message: string
}
interface iAuthor {
name: string,
email: string,
date: number
}
// eslint-disable-next-line #typescript-eslint/explicit-module-boundary-types
const CommitList = (props: iProps) => {
const repoName = props.repoName
const [commitList, setCommitList] = useState<iCommits | []>([])
useEffect(() => {
axios
.get(`https://api.github.com/repos/mathiusjohnson/${repoName}/commits`)
.then(res => {
setCommitList(res.data)
})
}, [repoName])
const commitKeys = Object.keys(commitList)
console.log(commitList[commitKeys[0] as keyof iCommits]);
const renderedCommits = commitKeys !== undefined && commitKeys.length > 0
? (commitKeys as string[]).map((commit: string, index: number) => {
if (index !== 0) {
return null
} else {
const startTimeISOString = commitList[commit as keyof iCommits].commit.author.date
const startTimeDate = new Date(startTimeISOString).toDateString()
console.log("latest commit: ", startTimeDate);
return (
<li key={index}>
<p>Author: {commitList[commit].commit.author.name}</p>
<p>Committed on: {startTimeDate}</p>
<p>Commit message: {sortedCommits[commit].commit.message}</p>
</li>
)
}
})
: ''
return (
<div>
<h2>Most recent commit</h2>
<ol>{renderedCommits}</ol>
</div>
)
}
export default CommitList
You can test the axios call out if you need to with this url (not related to this current project, just the first one I thought of to share that is not private):
https://api.github.com/repos/mathiusjohnson/social_network_api/commits
My issue right now I'm getting is this error:
Element implicitly has an 'any' type because expression of type 'string | number' can't be used to index type '[] | iCommits'.
No index signature with a parameter of type 'string' was found on type '[] | iCommits'.ts(7053)
On any code that I'm trying to use a key in the commitList in code block below, like this console.log:
console.log(commitList[commitKeys[0] as keyof iCommits]);
Again, any overall feedback for improvement is greatly appreciated. Thanks in advance!
There's some logic which is not required in your sample file.
Additionally you expected commitList to be ICommits which is a object - that is incorrect.
commitList is a array of ICommit(based on this api call https://api.github.com/repos/mathiusjohnson/social_network_api/commits).
Interface names start with a Uppercase I so I changed e.g iCommit to ICommit.
A functional example of your code can be found https://codesandbox.io/s/pensive-heisenberg-ejmgk?file=/src/types.ts
I left some of your code intentionally so that you can see what changes I did and commented above it that it can be deleted(once you delete that code there are no more errors/warnings in this sample project).
tldr: https://codesandbox.io/s/pensive-heisenberg-ejmgk?file=/src/types.ts

Resources