I've run into an issue with combineReducers not being strict enough and I'm not sure how to get around it:
interface Action {
type: any;
}
type Reducer<S> = (state: S, action: Action) => S;
const reducer: Reducer<string> = (state: string, action: Action) => state;
const reducerCreator = (n: number): Reducer<string> => reducer;
interface ReducersMapObject {
[key: string]: Reducer<any>;
}
const reducerMap: ReducersMapObject = {
test: reducer,
test2: reducerCreator
}
I would expect reducerMap to throw an error because reducerCreator isn't a reducer (it's a function that takes a string and returns a reducer), but TypeScript is fine with this.
It seems that the source of the issue is that Reducer essentially boils down to any => any because functions with fewer parameters are assignable to functions that take more params..
This means that the ReducersMapObject type is basically just {[key: string]: function}
Is there a way to make the Reducer type stricter about requiring both parameters or another way to get more confidence that the ReducersMapObject actually contains reducer functions?
This code all compiles in the TypeScript playground if you're trying to replicate
Nice question... there are two viable options toward the end of this rather long answer. You asked two questions, I answered each separately.
Question 1:
It will be difficult to achieve that, because of two obstacles in TypeScript functions.
Obstacle 1: Discarding Function Parameters
One obstacle, which you already noted, is documented here under the heading "Comparing Two Functions." It says that "we allow 'discarding' parameters." That is, functions with fewer parameters are assignable to functions with more parameters. The rationale is in the FAQ. In short, the following assignment is safe because the function with fewer parameters "can safely ignore extra parameters."
Obstacle 2: Function Parameter Bivariance
A second obstacle is that function parameters are bivariant. That means we cannot work around this problem with a user-defined type parameter. In some languages, we could define
Pairalong with a function that accepts aPair.In languages with covariant functions, the above would restrict arguments to subtypes of
Pair.In TypeScript, simple type assignment follows expected substitutability rules, but functions follow bivariant rules. In its simple type assignment, TypeScript allows us to assign type
Pairto typeSinglebut not to assign typeSingleto typePair. This is an expected substitution.TypeScripts functions, though, are bivariant, and are not held to the same restrictions.
The result is that a function that expects a
Pairwill accept aSingle.Implications for
ReducersNeither of the following two techniques will enforce the number of parameters (or class properties) that
Reducerimplementations must accept.That probably will not be a problem practically, because letting
ReducersMapObjectaccept aReducerwith fewer parameters is safe. The compiler will still ensure that:Reducerincludes all theReducerarguments, andReduceronly operates on its (possibly short) parameter list.Question 2:
One thing that we're trying to do is to make the
reducerCreatorfunction (and other functions of unusual shape) incompatible with theReducer<S>function type. Here are two viable options.Viable option 1: User-defined Parameter Type
The second technique from above, using a user-defined type called
ReducerArgs<S>, will give us more confidence. It will not provide complete confidence, because we will still have bivariance, but it will ensure that the compiler rejectsreducerCreator. Here is how it might look:Viable Option 2: Generics and Union Types
Another option is to use a generic
ReducerMapObject<T>like this:And then to parameterize it with a union type that lists the types of all the reducers.
The result will be that
any => anybecomesT => T, whereTis one of the types listed in the union. (As an aside, it would be great to have a type that says, "x can be any type, so long as it is that same type as y.")While both of the above involve more code and are a bit clunky, they do serve your purpose. This was an interesting research project. Thank you for the question!