Reputation: 13643
Note:
The following code defines a type ListBuilder<TItem>
that works similarly to a StringBuilder
. Note how the add()
method supports several overloads, distinguished using the representation
parameter.
type ListBuilder<TItem> = {
add(item: TItem): void,
add(representation: 'item', item: TItem): void,
add(representation: 'list', items: TItem[]): void,
get(): TItem[];
};
The type DynamicListBuilder<TItem>
extends ListBuilder<TItem>
, adding two more overloads:
type DynamicListBuilder<TItem> = ListBuilder<TItem> & {
add(representation: 'itemFactory', factory: () => TItem): void,
add(representation: 'listFactory', factory: () => TItem[]): void,
};
Now let's add a utility type ItemFor<TListBuilder>
that gives us the TItem
type for a given list builder type:
type ItemFor<TListBuilder> = TListBuilder extends ListBuilder<infer TItem> ? TItem : never;
This utility type doesn't work as expected. The types it infers are too wide:
declare const myListBuilder: DynamicListBuilder<number>;
// Expected: number
// Actual: number | "list" | (() => number)
type Inferred = ItemFor<typeof myListBuilder>;
// This assignment shouldn't compile, but it does
const test: ListBuilder<number | "list" | (() => number)> = myListBuilder;
test.add(() => 42); // This method call shouldn't compile, but it does
Why is this? How can I get the ItemFor<TListBuilder>
type to work as expected?
Upvotes: 1
Views: 43
Reputation: 33061
This is because how you defined ListBuilder
.
There is a big difference between
type ListBuilder<TItem> = {
add(item: TItem): void,
get(): TItem[];
};
AND
type ListBuilder<TItem> = {
add:(item: TItem) => void,
get:() => TItem[];
};
First example uses method syntax and methods are bivariant in TypeScript. This is by design. It is for easier migrating from js to ts.
The second example add:(item: TItem) => void
uses function notation. This is much safer to use arrow function notation than methods.
COnsider this example
type Overloading<T> =
& ((item: T) => void)
& ((representation: 'item', item: T) => void)
& ((representation: 'list', items: T[]) => void)
type ListBuilder<T> = {
add: Overloading<T>
get(): T[];
};
type DynamicListBuilder<TItem> = ListBuilder<TItem> & {
add(representation: 'itemFactory', factory: () => TItem): void,
add(representation: 'listFactory', factory: () => TItem[]): void,
};
type ItemFor<TListBuilder> = TListBuilder extends ListBuilder<infer TItem> ? TItem : never;
declare const myListBuilder: DynamicListBuilder<number>;
// Expected: number
type Inferred = ItemFor<typeof myListBuilder>;
// Error
const test: ListBuilder<number | "list" | (() => number)> = myListBuilder;
type O<T> = T extends DynamicListBuilder<number> ? true : false
type Result = O<ListBuilder<number | "list" | (() => number)>>
test.add(() => 42);
test
has failed, type Inferred
infered as a number
.
Try to avoid this syntax: add(item: TItem): void
.
As you might have noticed, I have overloaded add
method in other way. Intersection of function produces overloads.
Here you can find more about bivariance
Under --strictFunctionTypes the first assignment is still permitted if compare was declared as a method. Effectively, T is bivariant in Comparer because it is used only in method parameter positions.
interface Comparer<T> {
compare(a: T, b: T): number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
Upvotes: 1