Andree Christian
Andree Christian

Reputation: 467

How to fix "Argument of type ... is not assignable to parameter of type ..." error?

Given

abstract class A {
  constructor() {
    this.initialize()
  }

  initialize<T extends {
    [t in keyof this]?: boolean
  }>(todos?: T) {
    // Do something
  }
}

class B extends A {
  initialize() {
    super.initialize({ // <-- it will throw error starting on this open bracket
        test: false
    })
  }

  test() {
    return 'test'
  }
}

Why is it the code above throwing error stating that { test: false } is not assignable to {[t in keyof this]?: boolean}? While it clearly is.

'test' is one of the keys of B, right? and keyof this will refer to key of B, right?

Upvotes: 2

Views: 2911

Answers (2)

hackape
hackape

Reputation: 19947

Proper Abstract Class Definition

First, you are "cheating".

abstract class A {
  constructor() {
    this.initialize()
    //   ~~~~~~~~~~ this `initialize` is from subclass
  }

  // different from this one here
  initialize<T extends {
    [t in keyof this]?: boolean
  }>(todos?: T) {
    // Do something
  }
}

You just trick TS to think you're calling the same initialize method, by give them the same name and compatible function signature (the optional todos param).

What you're trying to do should really be written as:

abstract class A {
  constructor() {
    this.initialize()
  }

  // fix 1: delcare abstract method
  abstract initialize(): void

  // fix 2: rename, mark as protected
  protected _initialize<T extends {
    [t in keyof this]?: boolean
  // fix 3: todos probably isn't optional
  }>(todos: T) {
    // Do something
  }
}

class B extends A {
  initialize() {
    super._initialize({
        test: false
    })
  }

  test() {
    return 'test'
  }
}

Connection between Subclass and Base Class

It's one way. Subclasses know about base class, but base class is not obligated to know about all subclasses that extend itself. Because each subclass can implement something different, how is base class suppose to know all these info?

That's the whole point of the abstract class thing. You declare abstract method or property on base class, that's effectively a contract established by base class. Whichever subclass wants to extend me? Sign the contract first! Comply with my terms!

Back to your case, why is _initialize in A supposed to know any thing about B? There could be C or D extending A too. It's just not possible to infer the keyof subclasses merely from this.

So you need to tell super._initialize about what subclass is this when calling. It's a requirement, not a limit of TS.

abstract class A {
  constructor() {
    this.initialize()
  }

  abstract initialize(): void

  // pass subclass type as generic param B
  protected _initialize<B>(todos: {
    [t in keyof B]: boolean
  }) {
    // Do something
  }
}

class B extends A {
  initialize() {
    super._initialize<B>({
      test: true,  // <-- this is correct now
      foobar: true // <-- this triggers error report
    })
  }

  test() {
    return 'test'
  }
}

You can do a lot of hacky stuff in JS, but don't confuse JS with TS. TS is mostly about best practice, and you hit all sort of quirk and error when you break them. Your case is not a limitation of TS, that's TS trying to persuade you back to the right track.

Upvotes: 1

Andree Christian
Andree Christian

Reputation: 467

@JGoodgive How then i could have a real reference to current class? As i need it to point current class for later use in class' descendants. I can do this by:

abstract class A<Model> {
  constructor() {
    this.initialize()
  }

  initialize<T extends {
    [t in keyof Model]?: boolean
  }>(todos?: T) {
    // Do something
  }
}

class B extends A<B> {
  initialize() {
    super.initialize({
      test: true,
      initialize: false,
    })
  }

  test() {
    return 'test'
  }
}

But it seems silly to pass in B into B, don't you think?

Upvotes: 0

Related Questions