This is a weird one… ????
Scenario:
Say I have a base class with two or more classes:
// Base class
class Something <T> {
constructor (x: T) {
// ...
}
doSomething (value: T) {
// ...
}
}
// Subclasses
class AnotherThing extends Something<string> {}
class YetAnotherThing extends Something<number> {}
// Union of subclasses
type SomethingUnion = AnotherThing | YetAnotherThing;
Now say that there is an object type which has values of type SomethingUnion
…
type SomethingMap = {
[key: string]: SomethingUnion;
}
…and a mapped type that extracts the type parameter from each member of a given SomethingMap
(here DataOf
):
// A type that extracts `T` from `Something<T>`.
type GetT<S extends Something<any>> = S extends Something<infer U> ? U : never
// A mapped type that extracts the type parameter from every `Something` subclass.
type DataOf<T extends SomethingMap> = {
[K in keyof T]: GetT<T[K]>;
}
Issue:
If I have some value someMap
of type SomethingMap
, and another value someOtherMap
, which is a DataOf
type created from someMap
, and I want to map over someMap’s entries, TypeScript infers the type of thing.doSomething(...)
‘s parameter to be of type never
, when I would like the type to instead be the same as the type of valueToDoThingsWith
:
// Note: someMap declared previously with type `SomethingMap`
// Note: someOtherMap declared previously with type equivalent to `DataOf<typeof someMap>`
const entries = Object.entries(someMap); // [string, SomethingUnion][]
const mappedEntries = entries.map(([key, thing]) => {
const valueToDoThingsWith = someOtherMap[key] // string | number
// value has the type `SomethingUnion`, and the parameter `value` is `never`, which
// does not work with `valueToDoThingsWith`, which holds the intended type "string | number".
thing.doSomething(valueToDoThingsWith)
// ...
})
Is there a way to work around such a issue, and to have doSomething
take a parameter of the same type as valueToDoThingsWith
?
Here are two potential solutions:
Option 1:
Solution:
Use GetT<...>
to generate a union of the types of T
in each Something<T>
subclass, rather than using a union of the subclasses themselves.
// This will create a union of `GetT<...>` results for each constituent of SomethingUnion
type TypesOfT = GetT<SomethingUnion>;
Then, use this TypesOfT
type as the type argument to Something<...>
:
type BetterSomethingUnion = Something<TypesOfT>;
Why this works:
Consider the map
call:
// Note: `someMap` and `someOtherMap` have the types discussed above.
const entries = Object.entries(someMap); // [string, SomethingUnion][]
const mappedEntries = entries.map(([key, thing]) => {
const valueToDoThingsWith = someOtherMap[key] // string | number
// `thing` is of type `SomethingUnion`
thing.doSomething(valueToDoThingsWith) // error
// ...
})
In the callback function, thing
has the type SomethingUnion
, which is the same as:
type SomethingUnion = AnotherThing | YetAnotherThing;
type SomethingUnion = Something<string> | Something<number>;
However, to generate the desired result, a type along the lines of Something<string | number>
is needed, since the argument to .doSomething(...)
would then be typed string | number
, which is the desired behaviour.
TypeScript cannot assign Something<string> | Something<number>
to Something<string | number>
, since they are not the same in some cases. This means that the argument to thing.doSomething(...)
evaluates to never
.
Using the type BetterSomethingUnion
, as described in the solution, will generate the following:
type TypesOfT = GetT<SomethingUnion>; // string | number
type BetterSomethingUnion = Something<TypesOfT>; // Something<string | number> !!!
Which is exactly what is needed.
So, a type such as the following could be used to replace SomethingMap
:
type BetterSomethingMap = {
[key: string]: BetterSomethingUnion;
};
Option 2 (not recommended)
Solution
Simply cast valueToDoThingsWith
to never
(this must be repeated with every call to .doSomething(...)
, however, and my likely cause more issues later on):
// Regular TypeScript (`.ts`) file only:
thing.doSomething(<never>valueToDoThingsWith);
// OR
// Regular TypeScript (`*.ts`) files, and TypeScript JSX (`*.tsx`) files:
thing.doSomething(valueToDoThingsWith as never);
Why this works:
Casting valueToDoThingsWith
to never
means that TypeScript will consider valueToDoThingsWith
to be assignable to the argument of thing.doSomething(...)
, which expects never
as the parameter type, so that typechecking errors occur.
Why this is not recommended:
Although casting to never solves the issue described in the question, it may not cover other issues which may rise from using SomethingMap
rather than BetterSomethingMap
(or an equivalent type), and can cause issues when maintaining the code as well, since the root cause of the error has not been properly addressed.