Infering generics not at instantation, but later / updating a generic's type

Viewed 61

Given the example below, can one get the typescript compiler to understand that baz's return type, must be string as it can be inferred from foo.a('aString') that it's a string?

const fn = <T,S>()=>{
  let s: S
  let t: T
  return {
    a: <Z extends T=T>(arg: Z)=>{
      t = arg
      return s
    },
    b: <V extends S=S>(arg:V)=>{
      s = arg
      return t
    }
  }
}
const foo = fn()
const bar = foo.a('aString')
// currently,baz's type is 'unknown', but it is determinable that the type should be 'string'
const baz = foo.b('aString') 

code

A solution is to define const foo = fn<string, string>(). However, this requires one to specify both types upfront when foo.a('aString') may be the best place to infer T, rather than having to explicitly specify the type.

I suspect not ... but one can have hopium!

2 Answers

fn functions emulates class. TS has good support for classes, hence I think it worth using class in your example. See this approach:

class fn<T, S>{
  s: S | undefined = undefined
  t: T | undefined = undefined

  constructor() { }

  a<Z extends T = T>(arg: Z): asserts this is this & { t: Z } {
    this.t = arg

  }
  b<V extends S = S>(arg: V): asserts this is this & { s: V } {
    this.s = arg
  }

}
const foo: fn<string, string> = new fn<string, string>()

foo.a('aString')
foo.t // 'aString'

foo.b('bString')
foo.s // 'bString'

Playground

If you mutate this in your class and you want to track this mutation, you need to use assertion functions.

However, it does not work in your example because you call fn without explicit generic arguments. If TS is unable to infer toy of generic it uses unknown as a default type.

See this function:

const bar = <T,>() => {
  return 42 as any as T
}
//const bar: <unknown>() => unknown
bar()

T can't be infered, because it was not provided. Same case is with fn function

A workaround solution:

const fn = <T,S>(t?: T, s?: S)=>{
  return {
    a<NewT>(arg: NewT) {
      return fn<NewT,S>(arg,s)
    },
    b<NewS>(arg: NewS) {
      return fn<T,NewS>(t,arg)
    },
    t(){
      return t as T // removed undefined for simplicity
    },
    s(){
      return s as S // removed undefined for simplicity
    }
  }
}
const foo = fn()
const bar = foo.a('aString')
const t = bar.t() // string
const s = bar.s() // unknown
const baz = foo.b('aString') 
const t1 = baz.t() // unknown
const s1 = baz.s() // string
const foobar = bar.b('aString') 
const t2 = foobar.t() // string
const s2 = foobar.s() // string
Related