1

My goal is that foo accepts any object which extends an Obs<T>. However I get the following error.

Argument of type '{ reset: () => number; x: number; set: (x: number) => number; }' is not assignable to parameter of type 'Obs<unknown>'.
  Object literal may only specify known properties, and 'reset' does not exist in type 'Obs<unknown>'

Snippet A

{
  // add a generic method 
  type Obs<T> = { x: T; set: (x: T) => T }
  function createObs<T>(x: T): Obs<T> {
    return { x, set: (val: T) => val }
  }
  function foo<O extends Obs<T>, T>(a: O) {}
  const a = createObs(1)
  foo({ ...a, reset: () => 1 }) // ❌ now throws the error
}

Interestingly if I remove the set method the types work as expected like so.

Snippet B

{
  type Obs<T> = { x: T }
  function createObs<T>(x: T): Obs<T> {
    return { x }
  }
  function foo<O extends Obs<T>, T>(a: O) {}
  const a = createObs(1)
  foo({ ...a, reset: () => 1 }) // ✅
}

TS playground link

How do I make Snippet A work with the generic set method?

david_adler
  • 9,690
  • 6
  • 57
  • 97

2 Answers2

1

You need to add extra generic parameter:

// U is extra generic parameter
type Obs<T> = { x: T; set: <U extends T>(x: U) => T }

function createObs<T>(x: T): Obs<T> {
  return { x, set: (val: T) => val }
}
function foo<O extends Obs<T>, T>(a: O) { }
const a = createObs(1)
a.set(2) // ok
foo({ ...a, reset: () => 1 })

Playground

T is covariant in type Obs<T> = { x: T }. Consider this example:

type Obs<T> = { x: T }

let foo: Obs<number> = { x: 0 }

let bar: Obs<42> = { x: 42 }

foo = bar

bat is assignable to foo.

But if you add a set method, like here:

type Obs<T> = { x: T, set: (x: T) => T }

T becomes invariant.


type Obs<T> = { x: T, set: (x: T) => T }

let foo: Obs<number> = { x: 0, set: (val) => val }

let bar: Obs<42> = { x: 42, set: (val) => val }

foo = bar // error
bar = foo // error

bar is no more assignable to foo.

Hence, if you want to make it work, you need to make argument of set a subtype of T.

To be honest, I'm not sure about T becomes invariant. So, if you have any arguments about that I am open to criticism.

P.S. more about *-variance you will find here

1

The problems seems to be associated with the fact that you are not defining what will be the default value of T in the foo function.

If you infer T default type by whatever was passed into Obs, typescript will have just enough information to use the type you passed into the createObs function previously.

{
  type Obs<T> = { x: T; set: (x: T) => T };
  function createObs<T>(x: T): Obs<T> {
    return { x, set: (val: T) => val };
  }
  function foo<O extends Obs<T>, T = O extends Obs<infer R> ? R : never>(
    a: O
  ) {}
  const a = createObs(1);
  foo({ ...a, reset: () => 1 });
}
Pedro Figueiredo
  • 2,304
  • 1
  • 11
  • 18