#Argument of type 'Form<State2>' is not assignable to parameter of type 'Form<State1>'

10 messages · Page 1 of 1 (latest)

solid tartan
#

Property 'bar' is missing in type 'State1' but required in type 'State2'.

This is right on the edge of my type theory literacy.

I have two State records. State2 is just State1 with an extra prop (bar). I have a third Form type which is generic with State and has some extra props. I have a function that takes Form<State1>, and I can't pass Form<State2> to it.

I think this is because one of the derived Form props has a Map and it affects the variance of the function, but I'm not 100% clear on how that happens. I got a nice, minimal playground, though.

#

!ts

hybrid muralBOT
#
// Map of fields to current values
type FormState = Record<string, unknown>;

type State1 = {
    foo: string;
}
type State2 = {
    foo: string;
    bar: string;
}

type FormControllerState<T extends FormState> = {
    validity: Map<keyof T, boolean>; // This map is the root cause. Record works fine.
}

type Form<T extends FormState> = {
    getControllerState<S = FormControllerState<T>>(derive?: (arg: FormControllerState<T>) => S): S;
}

function isFooValid(form: Form<State1>): boolean {
    const fooIsValid = form.getControllerState(s => s.validity.get('foo'));
    return fooIsValid ?? false;
}
declare const myForm: Form<State2>;
isFooValid(myForm);
//         ^^^^^^
// Argument of type 'Form<State2>' is not assignable to parameter of type 'Form<State1>'.
//   Property 'bar' is missing in type 'State1' but required in type 'State2'.


/**
 * Current workaround
 */

type SafeForm<T extends FormState> = Omit<Form<T>, 'getControllerState'> & {
    getControllerState(): FormControllerState<T>;
}
function isFooValid2(form: SafeForm<State1>): boolean {
    const controllerState = form.getControllerState();
    return controllerState.validity.get('foo') ?? false;
}
isFooValid2(myForm);```
#
smichel17#0

Preview:ts // Map of fields to current values ...

signal flame
#

It seems that the problem at the root is

type A = 'foo'
type B = 'foo' | 'bar'
// B cannot be assigned to A

and the same applies to any generic type wrapping them, since for

type SomeGeneric<T> = { v: T }
type AA = SomeGeneric<A> // { v: 'foo' }
type BB = SomeGeneric<B> // { v: 'foo' | 'bar' }

clearly BB also cannot be assigned to AA, so there's nothing special about the Map generic type, and it would be natural that for

type MapA = Map<A, any> // Map<'foo', any>
type MapB = Map<B, any> // Map<'foo' | 'bar', any>

MapB cannot be assigned to MapA, although it may works fine in runtime
The Record works because TypeScript does know what a record would look like internally

type RA = Record<A, any> // { foo: any }
type RB = Record<B, any> // { foo: any; bar: any } 

So it knows RB can be assigned to RA by their internal type compatibility, which is rather a special case

#

Well I was being somewhat imprudent there by saying the same applies to any generic type. TypeScript does checks those generic definitions internally to see if there's anything preventing SomeGeneric<B> to be assignable to SomeGeneric<A>. Just like the v: T part.

It turns out in the Map's case what really hinders the assignment is the forEach definition

forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;

which can be simplified to

f(callback: (key: K) => void): void;
hybrid muralBOT
#
diff3usion#0

Preview:```ts
let a: {
f(callback: (key: string | number) => void): void
} = {f: callback => callback(1)}
let b: {f(callback: (key: string) => void): void} = {
f: callback => callback("hello"),
}

// @ts-ignore
b = a

// b.f(expectingString)

let aMap = new Map<string | number, unknown>()
...```

solid tartan
#

I suppose anything with an iterator has the same issue.

#

While I was thinking through this, I was briefly stumped since this type also causes the same error, but doesn't seem like it should: Pick<Map<keyof T, boolean>, 'set'>;. But it's because set returns the same map, which still has the .forEach method available