Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional types in constructor parameters cannot infer type from generics #50210

Closed
kaczor6418 opened this issue Aug 6, 2022 · 4 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@kaczor6418
Copy link

Bug Report

πŸ”Ž Search Terms

conditional types, generics

πŸ•— Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about 4.6.4 (also beta channel)

⏯ Playground Link

Conditional types in constructor parameters cannot infer type from generics

πŸ’» Code

I want to create an abstract class that will store some common logic and type will be inferred based on the generic passed to this abstract class.

Simple example

type StringOrNuber = string | number;

type ConditionalType<T> = T extends StringOrNuber ? { value: T } : T;

interface HandleBaseClass<T> {
  value: T
}

abstract class BaseClass<T> implements HandleBaseClass<T> {
  test: ConditionalType<T>;

  constructor(value: ConditionalType<T>) {
    this.test = value;
  }

  abstract get value(): T;

  abstract set value(newValue: T);
}

class ClassA<T extends StringOrNuber> extends BaseClass<T> {
  constructor(initialValue: T) {
    super({value: initialValue});
  }

  get value() {
    return this.test.value;
  }

  set value(newValue: T) {
    this.test.value = newValue;
  }
}

class ClassB<T extends Record<PropertyKey, unknown>> extends BaseClass<T> {
  constructor(initialValue: T) {
    super(initialValue);
  }

  get value() {
    return this.test;
  }

  set value(newValue: T) {
    this.test = newValue;
  }
}

Playground Link: Provided

In the above example I have a ConditionalType<T> that can be an object with value key only if we pass a number or string value, for all other cases the type should be the same as given type. Let's consider abstract class BaseClass<T> that takes one parameter in the constructor, which will be a type of { value: T } or T. We have created two more specific class ClassA and ClassB.

ClassA

ClassA extends BaseClass and adds restriction to its generic that it can be only sth that extends StringOrNuber type (string | number) so the BaseClass knows that T can be only a string or number so type of constructor parameter and test field should be { value: StringOrNumber }. This partly works. If I try to access this.test from ClassA then TS says the type of this.test is { value: number; } | { value: string; } what is good, but if I try to pass value that can be only a string or number wrapped in an object I am getting an error. What should not happen, I think (correct me if I am wrong)

abstract class BaseClass<T> implements HandleBaseClass<T> {
  test: ConditionalType<T>;

  constructor(value: ConditionalType<T>) {
    this.test = value;
  }
  // ...
}

class ClassA<T extends StringOrNuber> extends BaseClass<T> {
  constructor(initialValue: T) {
    super({value: initialValue});
  }
  // ...
}

Argument of type '{ value: StringOrNuber; }' is not assignable to parameter of type 'ConditionalType'

ClassB

ClassB extends BaseClass and adds restriction to its generic that it can be only sth that extends Record<PropertyKey, unknown> type (basically any object) so the BaseClass knows that T can NOT be a string or number so type of constructor parameter and test field should be T. This partly works. If I try to access this.test from ClassB then TS says the type of this.test is T what is good, but if I try to pass value that can be only an object I am getting an error. What should not happen, I think (correct me if I am wrong).

abstract class BaseClass<T> implements HandleBaseClass<T> {
  test: ConditionalType<T>;

  constructor(value: ConditionalType<T>) {
    this.test = value;
  }
  // ...
}

class ClassB<T extends Record<PropertyKey, unknown>> extends BaseClass<T> {
  constructor(initialValue: T) {
    super(initialValue);
  }
  // ...
}

Argument of type 'T' is not assignable to parameter of type 'ConditionalType'.
Type 'Record<PropertyKey, unknown>' is not assignable to type 'ConditionalType'.

If we use this ConditionalType<T> like that:

type ConditionalType<T> = T extends StringOrNuber ? { value: T } : T;

const x: ConditionalType<number> = {
  value: 12
}
const y: ConditionalType<Person> = {
  firstName: 'Jan',
  lastName: 'Kowlaski',
  age: 21
}

Then there is no any issues and types are inferred as expected.

To solve the above issue, I can use Union type and initialize test separately in ClassA and ClassB, but I would like to do it only once and do not repeat this operation in every class.

πŸ™ Actual behavior

Class constructor parameters do not infer generic type to use it in ConditionalType

πŸ™‚ Expected behavior

Class constructor parameters should infer generic type and use it in ConditionalType

@whzx5byb
Copy link

whzx5byb commented Aug 6, 2022

The constraint provided for ClassA is not applied to the conditional type in BaseClass, even they are literally the same. See discussions in #31096 and #32591.

@kaczor6418
Copy link
Author

The constraint provided for ClassA is not applied to the conditional type in BaseClass, even they are literally the same. See discussions in #31096 and #32591.

@whzx5byb So, this is impossible right now because of TypeScript limitations and this can be more like feature request than bug ?

@whzx5byb
Copy link

whzx5byb commented Aug 6, 2022

@kaczor6418 I think so. At the moment you can use type assertion super({value: initialValue} as ConditionalType<T>) as a workaround.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Aug 8, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants