2
votes

i am pretty new to react, react-hooks and js/ts.

At the moment i am coding a simple button which gets a state through useContext and updates that state with useReducer and a dispatch function.

I tried to seperate all of my code in specific files. The dispatch function from the reducer is passed as a value in the provider.

When i call the passed function in the consumer component i can give it a value of any type (number in my example) to the dispatcher. I set all types in the context, reducer, etc. VSCode is telling me the types which it expects - seems all correct. It still allows any type to get through. Why is that? I tried to console.log at all points where the value is passed, and it seems to override the type with type number, ignoring my specified types.

//app-state.tsx:
export type AppState = {
  readonly FoldoutPosition: number | null,
  readonly FoldoutType: FoldoutType | null
}

enum FoldoutType {
  Projects,
  Connection
}
//app-context.tsx
import { createContext, Dispatch } from 'react'
import { AppState} from '../states/app-state'

type ContextState =
  { AppState: AppState } &
  { changeFoldoutPosition: Dispatch<AppState["FoldoutPosition"]> } &
  { changeFoldoutType: Dispatch<AppState["FoldoutType"]> }


  export default createContext<ContextState>({
    AppState: {
      FoldoutType: null,
      FoldoutPosition: 0
    },
    changeFoldoutPosition: () => { },
    changeFoldoutType: () => { }
  });
//reducer.tsx
import { AppState } from '../states/app-state'


type Action =
    | { readonly type: 'CHANGE_FOLDOUTPOSITION', readonly payload: AppState["FoldoutPosition"] }
    | { readonly type: 'CHANGE_FOLDOUTTYPE', readonly payload: AppState["FoldoutType"] };


export const reducer = (state: AppState, action: Action): AppState => {

    switch (action.type) {
        case 'CHANGE_FOLDOUTPOSITION':
            return { ...state, FoldoutPosition: action.payload };
        case 'CHANGE_FOLDOUTTYPE':
                return { ...state, FoldoutType: action.payload };
        default:
            return state
    }

}
//wrapper.tsx
import React, { useReducer } from 'react'
import { reducer } from '../reducers/reducer'

import AppContext from './app-context'
import {AppState} from '../states/app-state'

const AppWrapper = (props: any) => {

  const initalState = {
    FoldoutType: null,
    FoldoutPosition: 15
  }

  const [state, dispatch] = useReducer(reducer, initalState);

  const changeFoldoutPosition = (FoldoutPosition: AppState["FoldoutPosition"]) => {
    dispatch({type: 'CHANGE_FOLDOUTPOSITION', payload: FoldoutPosition});
  }

  const changeFoldoutType = (FoldoutType: AppState["FoldoutType"]) => {
    dispatch({type: 'CHANGE_FOLDOUTTYPE', payload: FoldoutType});
  }

  return (
    <AppContext.Provider  
      value={{
        AppState: state,
        changeFoldoutPosition: changeFoldoutPosition,
        changeFoldoutType: changeFoldoutType
      }}
    >
      {props.children}
    </AppContext.Provider>
  );
}

export default AppWrapper;
/// component:
import React, { useContext } from 'react'
import AppContext from '../contexts/app-context'

const TestComponent = (props: any) => {

    const context = useContext(AppContext);

    return (
        //{context.AppState.Foldout}
        <div>
            <button onClick={() => context.changeFoldoutPosition(10)}>
                1 - {context.AppState.FoldoutPosition} - {context.AppState.FoldoutType}
                </button>
            // The following button onClick should not work
            // Type should be restricted to <FoldoutType | null>
            <button onClick={() => context.changeFoldoutType(23)}>
                2 - {context.AppState.FoldoutPosition} - {context.AppState.FoldoutType}
                </button>
        </div>
    );
}

export default TestComponent 
1

1 Answers

0
votes

This has nothing to do with React, it's a pure TypeScript issue. Enums in TypeScript have an auto-increment functionality that, if not specified, by default initializes the keys with numbers.

From the docs:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

Here, Up would have the value 0, Down would have 1, etc.

That's why your changeFoldoutType allows passing an integer as a parameter: TypeScript Enums allow it.

For your example, your TypeScript code..

type AppState = {
  readonly FoldoutPosition: number | null,
  readonly FoldoutType: FoldoutType | null
}

enum FoldoutType {
  Projects,
  Connection
}

const testFn = (foldoutType: AppState["FoldoutType"]) => {
    console.log(foldoutType)
}

testFn(1)

console.log(FoldoutType)
// { 
//   "0": "Projects",
//   "1": "Connection",
//   "Projects": 0,
//   "Connection": 1
// }

..compiles to JavaScript like so, where you can clearly see the numbers to access the Enum keys:

var FoldoutType;

(function (FoldoutType) {
    FoldoutType[FoldoutType["Projects"] = 0] = "Projects";
    FoldoutType[FoldoutType["Connection"] = 1] = "Connection";
})(FoldoutType || (FoldoutType = {}));

const testFn = (foldoutType) => {
    console.log(foldoutType);
};

testFn(1);

console.log(FoldoutType);