I am actively looking suggestion to better describe this issue. We discussed it over three days now in the Gitter channel. Most recent reference at the time of writing these lines is :point_up: April 28, 2019 10:26 PM
TypeScript Version: 3.5.4
Search Terms: Type 'T' is not assignable to type '
Code
// import { Action } from 'redux';
type Action<T> = { type: T }
const actions: ActionMap<Type> = {
setIsPublic: ( payload: boolean ) => ({ type: 'z' as Type, payload }),
// ^^^^^^^^^^^
};
interface ActionMap<T extends string>
{
[ name: string ]: <P>( payload: P ) => Action<T> & { payload: P };
}
enum Type { SET_PUBLIC = 'SET_PUBLIC', SET_PRIVATE = 'SET_PRIVATE' };
Expected behavior: No error
Actual behavior: Error at setIsPublic
Type '(payload: boolean) => { type: Type; payload: boolean; }' is not assignable to type '<P>(payload: P) => Action<Type> & { payload: P; }'.
Types of parameters 'payload' and 'payload' are incompatible.
Type 'P' is not assignable to type 'boolean'.
Playground Link: Follow me
Related Issues: No
This is a correct error. ActionMap's definition says this is legal:
actions["setIsPublic"](10);
but that call would violate setIsPublic's call signature.
It sounds like what you want is a definition that says that every key in ActionMap points to a function where the return type of that function is related to its concrete parameter type as stated by its definition rather than at its call site. I don't think there's a way to do that in this context.
@SalathielGenese Might I suggest a workaround to get the desired behavior:
type Action<T> = { type: T }
const actions = createActionMap<Type>()({
setIsPublic: ( payload: boolean ) => ({ type: Type.SET_PUBLIC, payload }),
})
function createActionMap<T extends string>(){
return function<M>(o: ActionMap<T, M>) {
return o;
}
}
type ActionMap<T extends string, M> =
{
[K in keyof M]: ( payload: M[K] ) => Action<T> & { payload: M[K] };
}
enum Type { SET_PUBLIC = 'SET_PUBLIC', SET_PRIVATE = 'SET_PRIVATE' };
The createActionMap will enforce that the payload has the same type as the parameter.
Tx for looking into this @RyanCavanaugh - But I'm not sure to get the difference from definition to call site.
= = =
Thank you too @dragomirtitian, that's quite impressive. I was able to complete my type to my flavour
type Action<T> = { type: T }
const actions = createActionMap<Type>()({
setPublic: () => ({ type: Type.SET_PUBLIC }),
setIsAge: ( payload: number ) => ({ type: Type.SET_PUBLIC, payload }),
setIsPublic: ( payload: boolean ) => ({ type: Type.SET_PUBLIC, payload }),
})
function createActionMap<T extends string>(){
return function<M>(o: ActionMap<T, M>) {
return o;
}
}
type ActionMap<T extends string, M> =
{
[K in keyof M]:
{ (): Action<T> } |
{ ( payload: M[K] ): Action<T> & { payload: M[K] } }
}
enum Type { SET_PUBLIC = 'SET_PUBLIC', SET_PRIVATE = 'SET_PRIVATE' };
Ok @RyanCavanaugh , I think the following excerpt is related to the initial issue/concern.
let identity = <I>( i: I ) => i;
let lowercase = (string: string) => string.toLowerCase();
identity = lowercase;
// ^^^^^^^^
// Type '(string: string) => string' is not assignable to type '<I>(i: I) => I'.
// Types of parameters 'string' and 'i' are incompatible.
// Type 'I' is not assignable to type 'string'.
Why is TypeScript trying to compare my call site string to my definition generic I ?
Is it not kinda dummy as reasoning for TypeScript.
I argued that what I mean is :
antything, even a
string
But in our discussion (maybe even few messages before), @G-Rath answered :
anything, not just a
string
I'm not sure what this means in TypeScript compiler words.
@SalathielGenese I'm not sure why you would expect identity = lowercase; to work, if we try to use identity we are sure to get an error:
let identity = <I>( i: I ) => i;
let lowercase = (string: string) => string.toLowerCase();
identity = lowercase; // If this were allowed
identity(1) // this would ok, since identity is generic, so I is decided by the caller, but it would cause a runtime error
Generics give the caller (ie the call site) control over what I will be. As such any function that is an implementation for the generic function <I>( i: I ) => I must be able to handle any possible I passed into it. lowercase can only handle string. Typescript is right to scream at this.
@RyanCavanaugh had a fun tweet about this:
Why is this code illegal?
function func<T extends string>(x: T): T { return ""; }Every drink in a bar comes in a glass (the constraint), but the waiter (the function) can't just bring you an empty glass (the return value) if you asked for a beer (T)
Same thing here, the bartender (identity) needs to serve you what you ordered (for example number) it can't just decide to serve you string.
Many thanks @dragomirtitian , your approach was fun, clear and concise.
I got the missing point. Thank you.
Most helpful comment
@SalathielGenese I'm not sure why you would expect
identity = lowercase;to work, if we try to useidentitywe are sure to get an error:Generics give the caller (ie the call site) control over what
Iwill be. As such any function that is an implementation for the generic function<I>( i: I ) => Imust be able to handle any possibleIpassed into it.lowercasecan only handlestring. Typescript is right to scream at this.@RyanCavanaugh had a fun tweet about this:
Same thing here, the bartender (
identity) needs to serve you what you ordered (for examplenumber) it can't just decide to serve youstring.