Consider the following:
// external file
export const someFunction = setState => {
setState({ value: "some new string" })
}
// component's file
import { someFunction } from "pathToFile"
interface TState {
value: string
}
const [state, setState] = useState<TState>({ value: "some string" })
useEffect(() => {
someFunction(setState)
}, [])
Is it possible to achieve that someFunction will accept only this particular setState function as а parameter? If so, how it could be done?
To be clear, I want:
someFunction(setState) // pass
someFunction(setAnotherState) // fail
someFunction(anotherFunctions) // fail
someFunction() // fail
someFunction(0) // fail
someFunction(true) // fail
someFunction({}) // fail
Basically, you are asking for a nominal type:
type NominalSetState = React.Dispatch<React.SetStateAction<TState>> &
{ readonly __brand_setState__: unique symbol }
const someFunction = (setState: NominalSetState) => {
setState({ value: "some new string" })
}
function useNominalState(init: TState) {
return useState<TState>(init) as [TState, NominalSetState]
}
const [state, setState] = useNominalState({ value: "some string" })
const [anotherState, setAnotherState] = useState<TState>({ value: "some string" })
Now, tests behave as desired:
someFunction(setState) // pass
someFunction(setAnotherState) // fail
someFunction() // fail
someFunction(0) // fail
someFunction(true) // fail
someFunction({}) // fail
someFunction((...arg: any[]) => any) // fail
Live code example
You can achieve your goal by implementing the callback methods.
Example:
this.readStudentFile(anyFile, this.callback);
callback(isPass) {
this.setState({ isPass: isPass });
}
readStudentFile(file: File, callback) {
if(file) {
callback(true);
} else callback(false);
}
You could define a very specific parameter type to someFunction, which will give you the same pass/fails that you want.
Only allowing one particular object isn't what Typescript is really for - you are meant to guard by only allowing some interfaces, and anything with that same interface will be allowed.
Related
I'm using vue 3 and composable files for sharing some functions through my whole app.
My usePluck.js composable file looks like
import { api } from 'boot/axios'
export default function usePlucks() {
const pluckUsers = ({val = null, excludeIds = null}) => api.get('/users/pluck', { params: { search: val, exclude_ids: excludeIds }})
return {
pluckUsers
}
}
In order to make use of this function in my component I do
<script>
import usePlucks from 'composables/usePlucks.js'
export default {
name: 'Test',
setup() {
const { pluckUsers } = usePlucks()
onBeforeMount(() => {
pluckUsers({excludeIds: [props.id]})
})
return {
}
}
}
</script>
So far so good, but now I'd like to even be able to not send any args to the function
onBeforeMount(() => {
pluckUsers()
})
But when I do that, I get
Uncaught TypeError: Cannot read properties of undefined (reading 'val')
I assume it's because I'm not sending an object as argument to the function, therefore I'm trying to read val from a null value: null.val
What I'm looking for is a way to send, none, only one, or all arguments to the function with no need to set null values:
// I don't want this
pluckUsers({})
// Nor this
pluckUsers({val: null, excludeIds: [props.id]})
I just want to send only needed args.
Any advice about any other approach will be appreciated.
import { api } from 'boot/axios'
export default function usePlucks() {
const pluckUsers = ({val = null, excludeIds = null} = {}) => api.get('/users/pluck', { params: { search: val, exclude_ids: excludeIds }})
return {
pluckUsers
}
}
I believe this is what you're looking for. The { ... } = {}
EDIT: It didn't work without this because with no argument the destructuring failed because it can't match an object. That's why you also need a default value on the parameter object, also called simulated named parameter.
I'm testing a function which calls another function imported from anotherFile. That outsideFunc returns an object which contains 'name'. I need this to exist in order to progress through the rest of my test/the function to work correctly.
systemUnderTest.js
import { outsideFunc } from './anotherFile.js';
function myFunc() {
const name = outsideFunc().name;
}
anotherFile.js:
export function outsideFunc() {
return { name : bob }
}
I don't care about testing anotherFile or the result of outsideFunc, but I still need to return a mock value as part of testing myFunc;
systemUnderTest.spec.js
describe("A situation", () => {
jest.mock("./anotherFile", () => ({
outsideFunc: jest.fn().mockReturnValue({
name: 'alice'
})
}));
it("Should continue through the function steps with no problems", () => {
expect(excludeCurrentProduct(initialState)).toBe('whatever Im testing');
});
});
The problem I get is that, when the unit test is working through myFunc, const name returns undefined where it should return alice. I would expect it to get the data from my jest.mock of the anotherFile file and its mock exported function, but it doesn't get the right response.
When I asset that I expect name = alice I actually get name = undefined.
systemUnderTest.js
import { outsideFunc } from './anotherFile.js';
// let's say that the function is exported
export function myFunc() {
const name = outsideFunc().name;
// and let's say that the function returns the name
return name;
}
you can describe in your
systemUnderTest.spec.js
import { myFunc } from './systemUnderTest';
import { outsideFunc } from './anotherFile';
// using auto-mocking has multiple advantages
// for example if the outsideFunc is deleted the test will fail
jest.mock('./anotherFile');
describe('myFunc', () => {
describe('if outsideFunc returns lemons', () => {
outsideFunc.mockReturnValue({name: 'lemons'});
it('should return lemons as well', () => {
expect(myFunc()).toEqual('lemons');
});
});
});
working example
I have the following function definition:
const useScroll = () => {
const ref = useRef(null)
function executeScroll() {
if (ref !== null)
window.scrollTo(0, ref.current.offsetTop)
}
const htmlElementAttributes = { ref }
return [executeScroll, htmlElementAttributes]
}
export default useScroll;
Based on this function, I have the following code:
const [executeScroll, scrollHtmlAttributes] = useScroll();
const click_argueBtn = (e) => {
e.preventDefault();
executeScroll();//error
}
However, the executeScroll(); code throws the following error:
Error: Cannot invoke an expression whose type lacks a call signature
Any ideas why I receive this error? My code is based on this post.
Typescript is doing its best to determine the types automatically, and it determines that useScroll returns an array, who's elements are each () => void | { ref: /*ref type*/ } (i don't know precisely what the type on the ref object is). When you try to call executeScroll, it doesn't know whether it's a function, or an object with a ref. So since it might not be a function, you're not allowed to call it.
Instead, i'd recommend explicitly telling typescript that useScroll returns a tuple. A tuple is similar to an array, except you explicitly declare how many elements it has and what their individual types are:
const useScroll = (): [() => void, { ref: /* ref type */ }] => {
// ...etc
}
or
const useScroll = () => {
// ... etc
return [executeScroll, htmlElementAttributes] as [() => void, { ref: /* ref type */ }];
}
or if you don't like it inline, you could extract it to a type:
export type UseScrollResult = [() => void, { ref: /* ref type */ }];
const useScroll = (): UseScrollResult => {
// ...etc
}
From this, typescript now knows that element 0 of the array is () => void, and therefore it's legal to call it as a function.
Let's say I have this generic flow type:
/* #flow */
type Cat<T> = {
get:()=>T
};
And I want to create a function that creates a cat:
const makeCat:<U>(getter:()=>U)=>Cat<U>
= (getter) => ({get:getter});
Flow gives me the following error:
Cannot assign function to `makeCat` because `U` [1] is incompatible with `U` [2] in the return value of property `get` of the return value.
I've tried several different ways to define the types of the passed in 'getter', but it's always the same error.
Try this. I broke up the logic of it in to a few extra steps to make it more understandable. The key part of this solution is using * to tell flow to "fill in the blanks" whenever the makeCat function is being used.
type Cat<T> = {
get: () => T
}
// define signature of "makeCat" function
type MakeCat<U> = (getter: () => U) => Cat<U>
// use * to infer the variable at usage
const makeCat: MakeCat<*> = getter => ({ get: getter })
// inferred as Cat<string>
const cat = makeCat(() => 'secret')
// inferred as string
const value = cat.get()
Based on the answer by #kindaro, but simplified to not having to define the intermediary function type, by simply using the regular "old-school" function declaration form:
type Cat<T> = { get: () => T };
function makeCat<U>(getter: () => U): Cat<U> {
return { get: getter };
}
// inferred as Cat<string>
const cat: Cat<string> = makeCat(() => 'secret');
// const cat: Cat<number> = makeCat(() => 'secret'); // Yields error
// inferred as string
const value: string = cat.get();
// const value: number = cat.get(); // Yields error
Flow Try link here
Problem Explanation:
I want to unit test a redux-saga using Jest. I'm doing this the way it is described in the example provided within the redux-saga docs: https://redux-saga.js.org/docs/advanced/Testing.html
Within my Saga I'm calling a selector function selectSet that returns a specific object from the application store:
export const selectSet = state => state.setStore.set
In my saga I'm trying to yield this selector function:
import { put, select } from 'redux-saga/effects'
import { selectSet } from '../selectors'
export function* getSet() {
try {
const set = yield select(selectSet)
yield put({ type: 'SET_SUCCESS', payload: { set } })
} catch (error) {
yield put({ type: 'SET_ERROR', payload: { error } })
}
}
Within my test there is no valid application store so I'd have to mock the function to return the expected object:
import assert from 'assert'
import * as AppRoutines from './AppRoutines'
import { put, select } from 'redux-saga/effects'
describe('getSet()', () => {
it('should trigger an action type "SET_SUCCESS" with a payload containing a valid set', () => {
const generator = AppRoutines.getSet()
const set = {
id: 1,
slots: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}
const selectSet = jest.fn()
selectSet.mockReturnValue(set)
// Saga step 1
const actualFirst = generator.next().value
const expectedFirst = select(selectSet)
assert.deepEqual(
actualFirst,
expectedFirst,
'it should retreive a valid set from the store using the selectSet selector'
)
})
})
However - if I assert the saga to return a specific generator value using deepEqual and my mocked function, it expects my selector function to have the original selectSet constructor. But since I'm mocking the function with jest.fn() the constructor is actually equal to mockConstructor - which makes my test fail:
Expected value to deeply equal to:
{"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function mockConstructor]}}
Received:
{"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function selectSet]}}
Question: How can I make an assert.deepEqual containing a mock function without conflicting constructor types?
Alternative Question: Is there a way to make my assertion expect a mockConstructor instead of the actual selectSet constructor?
You should not need to mock the selector at all, as in a saga test of this nature, the selector is never actually called, instead you are testing the declarative instructions that are created for the redux saga middleware to act upon are as you expect
This is the instruction that the saga will create {"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function selectSet]}}, but as the middleware is not running during this test scenario selectSelect will never actually get called
If you need to mock results that your selector returns for your action, then you do so by passing the mock data into the next step...
const set = {
id: 1,
slots: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}
// Saga step 1
const firstYield = generator.next().value
assertDeepEqual(firstYield, select(selectSet))
// Step 2 - successful so dispatch action
// mock data from the previous yield by passing into this step via `next(stubbedYieldedData)`
const secondYield = generator.next(set).value
assertDeepEqual(secondYield, put({type: 'SET_SUCCESS', payload: {set} }))
We can pass the mock store in a fake store as below. Below are the sample selector and generator function along with its test.
Selector
const authSelector = (state) => state.authReducer || initialState;
Saga Generator function
export function* getAuthToken(action) {
try {
const authToken = yield select(makeSelectAuthToken());
} catch (errObj) {}
}
Test Case
import { runSaga } from 'redux-saga'
const dispatchedActions = [];
const fakeStore = {
getState: () => ({ authReducer: { auth: 'test' } }),
dispatch: (action) => dispatchedActions.push(action)
}
await runSaga(fakeStore, getAuthToken, {
payload: {}
}).done;
expected case you can write here below this