Reputation: 4236
Trying to implement a typing where primitive properties are of type KnockoutObservable<Primitive>
in a recursive/nested way:
export type Primitive = string | number | boolean | undefined | null;
export type KnockoutMappedProperty<T> = T extends Primitive ? KnockoutObservable<T> : KnockoutMappedType<T>;
export type KnockoutMappedType<T> = {
[Property in keyof T]: KnockoutMappedProperty<T[Property]>;
};
interface KnockoutObservable<T> extends KnockoutSubscribable<T>, KnockoutObservableFunctions<T> {
(): T;
(value: T): void;
/*...*/
}
Calling the function without argument compiles:
const v1: KnockoutMappedType<{ prop: boolean }> = null;
const propValue: boolean = v1.prop();
Passing an argument does not compile as if (value: T): void;
would not exist in KnockoutObservable<T>
:
v1.prop(true); // Gives compiler error: Expected 0 arguments, but got 1
Intellisense tells me that v1.prop
is of type KnockoutObservable<true> | KnockoutObservable<false>
. Why is something split into a union type that should simply be KnockoutObservable<boolean>
?
The following compiles fine again:
const prop: KnockoutObservable<boolean> = v1.prop;
prop(true);
Why do I need to introduce a variable of type KnockoutObservable<boolean>
?
Upvotes: 2
Views: 210
Reputation: 249676
The problem is that conditional types distribute over unions as stated in the docs. While it might not be obvious at first, boolean
is a union, the union true | false
. This means that the mapped prop1
will be of type KnockoutObservable<true> | KnockoutObservable<false>
. This means the only callable signature will be the common one, the one without any parameters.
To stop the distributing behavior, you can clothe the type parameter in a tuple (since distribution occurs only over naked type parameters)
export type Primitive = string | number | boolean | undefined | null;
export type KnockoutMappedProperty<T> = [T] extends [Primitive] ? KnockoutObservable<T> : KnockoutMappedType<T>;
export type KnockoutMappedType<T> = {
[Property in keyof T]: KnockoutMappedProperty<T[Property]>;
};
interface KnockoutObservable<T> extends KnockoutSubscribable<T>, KnockoutObservableFunctions<T> {
(): T;
(value: T): void;
/*...*/
}
const v1: KnockoutMappedType<{ prop: boolean }> = null;
const propValue: boolean = v1.prop();
v1.prop(true); // ok
Upvotes: 2