user764754
user764754

Reputation: 4236

Argument not allowed with keyof ("Expected 0 arguments, but got 1")

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>?

Playground link

Upvotes: 2

Views: 210

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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

Related Questions