dphilipson / typescript-fsa-reducers

Fluent syntax for defining typesafe reducers on top of typescript-fsa.
MIT License
220 stars 16 forks source link

Interfere return type in .case doesn't work #20

Open MistyKuu opened 6 years ago

MistyKuu commented 6 years ago

Hey, I just tried to use the library but unfortunately there is no type check in .case method for return value.

If I implement my reducer like this:

const execute = (execute: Execute, action: ReduxAction): Execute => {
  if (isType(action, actions.calibrationSettingsDone)) return { ...execute, settings: action.payload.result };
...

it will not allow me to return settings1 property because it doesn't exist in Execute model and it's fine however in your library it's completely fine to return any object.

dphilipson commented 6 years ago

You're right. Unfortunately this is a limitation of TypeScript that I don't see a way to work around in this circumstance. Unknown properties on object literals are not checked in the return value of functions which are given as arguments because TypeScript thinks its fine to accept a function which produces a return type which is a subtype of the desired type. A demo of this behavior:

interface HasX {
    x: number;
}

function getHasX(): HasX {
    // Extra properties are errors.
    return { x: 1, y: 2 };
}

function callGetHasX(f: () => HasX) {
    f();
}

// No error here.
callGetHasX(() => ({ x: 1, y: 2 }));

If this failure is enough of problem for you to not use this library, I understand. However, here are a few workarounds:

  1. Use a variant of object spread from a library with a more restrictive signature.

    In my code, I use pureAssign from the pure-assign library instead of object spread or Object.assign. Its signature of

    function pureAssign<T>(baseObject: T, ...updates: Array<Partial<T>>): T

    prevents unknown properties from being added which are not in the base type. Not to say you have to use this library in particular, but using a version of object assignment with more restrictive typing can be a generally good idea when writing TypeScript code.

  2. Pull your handlers out into separate functions.

    You can choose to write your code as

    const reducer = reducerWithoutInitialState<Execute>()
       .case(actions.calibrationSettingsDone, handleCalibrationSettingsDone);
    
    const handleCalibrationSettingsDone =
       (execute: Execute, payload: CalibrationSettingsDonePayload): Execute => 
           ({ ...execute, settings:  payload.result });

    Some people prefer this style anyways because it makes the reducer look less cluttered, although it does require more typing.

I'll think about this for a bit to see if I can come up with a better solution. If not, I'll make a note in the README about this.