I've come to join the typescript wagon and i wantede to keep using this immutability-helper lib when im updating my redux store, but for some reason now i get this error when im trying to execute a update?:
[ts] Argument of type '{ flags: { hideChat: { $set: boolean; }; }; }'
is not assignable to parameter of type 'Spec'.
Object literal may only specify known properties, and 'flags' does not
exist in type 'Spec'.
export interface GlobalStateInit {
flags: {
hideChat: boolean
}
}
const initialState: GlobalStateInit = {
flags: {
hideChat: false
}
}
const reducer: Reducer<GlobalStateInit, GlobalAction> = (state = initialState, action) => {
switch (action.type) {
case getType(actions.toggleChat):
return update(state, {
flags: {
hideChat: { $set: !state.flags.hideChat }
}
})
default:
return state
}
}
export { reducer as GlobalReducer }
I was asuming this should be trivial and should just work out of the box, my jest test running in the background can figure this out but the VScode TS Linter gets a bit angry.
Not sure if this is a bug or if its just VScode that messes up.
This is a deliberate compiler feature. Here's a short version of the same problem:
interface Example {
name: string,
val: number
}
const example: Example = {
name: 'Fenton',
val: 1,
more: 'example'
}
The more property isn't part of the Example interface, so the compiler is going to warn you that you might have made a mistake. For example, if there is an optional member that you typed wrong, it will catch that for you:
interface Example {
name: string,
val: number,
side?: string
}
const example: Example = {
name: 'Fenton',
val: 1,
sdie: 'Left' // Oops, I meant "side"
}
In cases where you think you know better than the compiler, you can tell it that you're in charge:
interface Example {
name: string,
val: number
}
const example = {
name: 'Fenton',
val: 1,
more: 'example'
} as Example
This allows additional members, while still ensuring you don't make a mistake such as:
// Lots of errors!
const example = {
more: 'example'
} as Example
Related
I have 2 types:
type Core = {
a: boolean;
b: number;
c: string
}
type CoreOptions = {
a?: boolean;
b?: number;
c?: string;
}
In this example, CoreOptions is meant to have some, none, or all of the properties that Core has depending on the use case, but never to have a property that isn't in Core.
I am trying to systematically tie them together so that as things evolve and Core gets new properties, I only have to change the type Core.
How can this be achieved?
To use a type, but make all properties optional, TypeScript has a Utility type called Partial. Official Documentation
Example from the TypeScript Documentation:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter",
};
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});
I have the following object on typescript to control results of a sports game:
export interface IResult {
[day: string]: IResultDay;
}
interface IResultDay {
[matchId: string]: IMatch;
}
interface IMatch {
teams: ITeam[];
winner: ITeam;
winnerScore: number;
loserScore: number;
}
I build it this way because it allow me to filter the matches I want to see per "day" and per "matchId".
The problem is that when I want to build this object, I need to keep verifying if its not undefined on each level. If I don't do that, the following happens:
const result: IResult = {}; // init the first level
result['2020-09-30']['2020093012344321'] = {
teams: [
{
teamId: '1234',
teamName: 'Lakers'
},
{
teamId: '4321',
teamName: 'Miami Heat'
}
],
winner: {
teamId: '1234',
teamName: 'Lakers'
},
winnerScore: 117,
loserScore: 107
};
The error:
TypeError: Cannot set property '2020093012344321' of undefined
And it get's worse when I go deep on the nested object. Is there a way to build this object without initializing each level?
In python there's this trick on dictionaries that allow us to start multiple levels at the same time:
from collections import defaultdict
my_dict = defaultdict(lambda: defaultdict(dict))
I want something that does exactly the same thing (or something with the same behavior). It's really weird keep initializing each level before creating the object in did.
Is there something like that on javascript/typescript?
There's nothing built in, but you can use a Proxy to intercept the property accesses and insert a default value when a property doesn't exist. Maybe something like this:
function defaultDict<T>(cb: (k: PropertyKey) => T): { [k: string]: T } {
return new Proxy({} as any, {
get(target, p, receiver) {
if (!(p in target)) {
target[p] = cb(p);
}
return target[p];
}
})
}
I don't know if the callback cares about the key passed in, but it doesn't really matter either way. Then you can make result like this:
const result: IResult = defaultDict(() => ({}));
which really only needs to have a single defaultDict() since to set result[day][match] = ... you really only need result[day] to be defined. And then this behaves as you expect, I think:
result['2020-09-30']['2020093012344321'] = {
teams: [
{
teamId: '1234',
teamName: 'Lakers'
},
{
teamId: '4321',
teamName: 'Miami Heat'
}
],
winner: {
teamId: '1234',
teamName: 'Lakers'
},
winnerScore: 117,
loserScore: 107
};
That gives no error, and then this works:
for (let day in result) {
for (let match in result[day]) {
console.log(JSON.stringify(result[day][match]))
}
}
/* {"teams":[{"teamId":"1234","teamName":"Lakers"},{"teamId":"4321","teamName":"Miami Heat"}],
"winner":{"teamId":"1234","teamName":"Lakers"},"winnerScore":117,"loserScore":107} */
Playground link to code
Unfortunately, no, javascript doesn't really have anything to allow setting multiple nested levels of a dictionary all in one go.
JavaScript sadly can't do this. However you can achieve this using some dependencies such as lodash:
const _ = require('lodash');
const result: IResult = {};
_.set(result, ['2020-09-30', '2020093012344321'], {} /* replace {} with your value */);
I've built multiple React functional components using Hooks and Context. Everything works fine. Now I need to write tests for everything. I'm confused about how to move forward with some of them so wanted to reach out to the community.
Actions
Here's a sampling from one of my Actions files:
export const ADD_VEHICLE: 'ADD_VEHICLE' = 'ADD_VEHICLE';
export const UPDATE_VEHICLE: 'UPDATE_VEHICLE' = 'UPDATE_VEHICLE';
type AddVehicleAction = {type: typeof ADD_VEHICLE, isDirty: boolean};
type UpdateVehicleAction = {type: typeof UPDATE_VEHICLE, id: number, propName: string, payload: string | number};
export type VehiclesActions =
| AddVehicleAction
| UpdateVehicleAction;
How am I supposed to test this Actions file? I don't mean in conjunction with anything else, I mean it and only it?
From the comments, it appears I have agreement that there's nothing to test DIRECTLY in this file.
Reducers
Each of my Reducers files is directly connected to and supports a specific Context. Here's a sampling of one of my Reducers files:
import type { VehiclesState } from '../VehiclesContext';
import type { VehiclesActions } from '../actions/Vehicles';
import type { Vehicle } from '../SharedTypes';
import { ADD_VEHICLE,
UPDATE_VEHICLE
} from '../actions/Vehicles';
export const vehiclesReducer = (state: VehiclesState, action: VehiclesActions) => {
switch (action.type) {
case ADD_VEHICLE: {
const length = state.vehicles.length;
const newId = (length === 0) ? 0 : state.vehicles[length - 1].id + 1;
const newVehicle = {
id: newId,
vin: '',
license: ''
};
return {
...state,
vehicles: [...state.vehicles, newVehicle],
isDirty: action.isDirty
};
}
case UPDATE_VEHICLE: {
return {
...state,
vehicles: state.vehicles.map((vehicle: Vehicle) => {
if (vehicle.id === action.id) {
return {
...vehicle,
[action.propName]: action.payload
};
} else {
return vehicle;
}
}),
isDirty: true
};
}
If you wanted to build tests for JUST this Reducers file, what approach would you use? My thought was to render the DOM like this:
function CustomComponent() {
const vehiclesState = useVehiclesState();
const { isDirty,
companyId,
vehicles
} = vehiclesState;
const dispatch = useVehiclesDispatch();
return null;
}
function renderDom() {
return {
...render(
<VehiclesProvider>
<CustomComponent />
</VehiclesProvider>
)
};
}
While this code above does run, I now have the problem that both vehiclesState and dispatch are not accessible within my test code so I'm trying to figure out how to "surface" those within each describe / it construct. Any suggestions would be appreciated.
Contexts
My Contexts follow the same pattern outlined by Kent C. Dodds: https://kentcdodds.com/blog/how-to-use-react-context-effectively - in that the StateContext & DispatchContext are separated, and there's a default state. Given this code pattern and given that I'll already have a separate test file for the Context's Reducers, what specifically could one test for ONLY for the Context?
Same as my comment, I really think you should read redux docs for writing tests so that you get a general idea of what to do.
But since you already have a reducer, you want to write your test case to follow this pattern
you will have at least 1 test per action
each test will have a a "previous state", which will be altered
you will call your reducer, passing the action and the previous state
you will assert your new state is the same as expected
Here's a code example:
it('adds a new car when there are no cars yet', () => {
// you want to put here values that WILL change, so that you don't risk
// a false positive in your unit test
const previousState = {
vehicles: [],
isDirty: false,
};
const state = reducer(previousState, { type: ADD_VEHICLE });
expect(state).toEqual({
vehicles: [{
id: 1,
vin: '',
license: '',
}],
isDirty: true,
});
});
it('adds a new car when there are existing cars already, () => {
// ...
});
I'd also recommend to use action creators rather than directly creating action objects, since it's more readable:
// actions.js
export const addVehicle = () => ({
type: ADD_VEHICLE
})
// reducer.test.js
it('adds a new car when there are no cars yet', () => {
//...
const state = reducer(previousState, actions.addVehicle());
In this piece of code, flow is not complaining about the value dog being set on the state. It seems to be ignoring the NamespaceData definition. I've set up the types so it should complain. I'm running on nuclide and flow is working properly for everything else.
All of the properties of action such as namespace, project, collection are strings.
// #flow
import { NAMESPACE_SET } from '../actions/NamespaceActions'
type NamespaceData = {
project: string,
collection: string,
}
type NamespaceState = {
[namespace: string]: NamespaceData,
}
const initialState: NamespaceState = {}
function namespaceReducer(state: NamespaceState = initialState, action: Object): NamespaceState {
switch (action) {
case NAMESPACE_SET: {
return {
...state,
[action.namespace]: {
project: action.project,
collection: action.collection,
dog: 1,
}
}
}
}
return state
}
export default namespaceReducer
Flow is not strict about unknown properties in objects by default, e.g.
// #flow
type Thing = {
known: string;
};
var obj: Thing = {
known: 'hi',
unknown: 4,
};
typechecks fine even though unknown is not in the type.
Flow 0.32 includes
New syntax for exact object types: use {| and |} instead of { and }. Where {x: string} contains at least the property x, {| x: string |} contains ONLY the property x.
In your example you'd want exact object syntax with:
type NamespaceData = {|
project: string,
collection: string,
|};
This should not compile, but it does:
var thingsWithName: { name: string }[] = [{ 'name': 'A' }, { 'name': 'B' }];
function doStuff <T extends { id: number }> (thingWithId: T): T {
return thingWithId;
}
thingsWithName.map(doStuff);
as you can see thingsWithName don't have an id, so typescript compiler should warn about this when passing the doStuff to map.
Why does this typecheck? Am I doing something wrong?
See this github issue.
The reason for this as outlined by the team is:
Our assignability relation ignores generics and constraints in signatures. It just replaces all type parameters with any.
... we ignore generics because we believe taking them into account will be slow. And in general it leads to never-ending recursion if the signature was in a generic type. Because of this it seems not worth it.
In your code, note that a non-generic version will throw an error:
function doStuff(thingWithId: { id: number }): { id: number } {
return thingWithId;
}
thingsWithName.map(doStuff); // error
And additionally note that since typescript uses structural typing to check the type, the following will happen with the non-generic version:
var arrayWithId = [{ id: 2, myOtherProperty: "other value" }],
arrayWithoutId = [{ noIdProperty: 2 }];
arrayWithId.map(doStuff); // ok
arrayWithoutId.map(doStuff); // error