Determine the return type of a method when using keyof

Want to create a Maybe <T> that will contain some object and do null / undefined check before accessing the object's properties. And, of course, you must enter resuls (Maybe <null> or Maybe <TRes>).

Here's an example:

class Maybe < T > {
  constructor(public value: T) {}
  static of < P > (obj: P): Maybe < P > {
    return new Maybe(obj);
  }

  map < TResult > (fn: (arg: T) => TResult): Maybe < TResult > {
    return this.isNothing ? nothing : new Maybe(fn(this.value));
  }

  public get < P extends keyof T > (name: P): Maybe < T[P] > {
    return this.isNothing ? nothing : Maybe.of(this.value[name]);
  }
  get isNothing(): boolean {
    return this.value == null;
  }
}
let nothing = new Maybe(null);

      

Everethisng works great here and is typed. For exaple:

class Test {
  a = {
    id: 1,
    name: "test1"
  };
  f = (foo: string) => {
    return new Test();
  };
};

let t = new Test();

console.log(Maybe.of(t).get('a').get('name').value); // ok 

      

But there is a problem with defining the "apply" function, which takes the name of an object property that is actually a function, executes that function, and returns Maybe <TResult>.

  // T[fName]: (...args)=> TResult
  public apply < P extends keyof T > (fnName: P, ...args: any[]) /*: Maybe<Tresult> */ {
    if (!this.isNothing) {
      let res = null;
      let fn = this.value[fnName];

      if (isF(fn)) {
        return Maybe.of(fn(...args));
      }

    }
    return nothing;
  }

      

Cannot find a solution to determine the result of the call "apply".

let fResult = Maybe.of(t).apply('f', 'foo');
// fResult is Maybe<any> ,  expected to be Maybe<Test>

      

Does anyone know how to determine the type for the "apply" result? Or is it even possible in TS 2.3?

Here's a link to a playground with the same code: TS playground

thank

+3


source to share


1 answer


First, let me defer the class Maybe<T>

and introduce some basic operations one step at a time.

You can have a general function that will check if an object is null and then return some property of that object. The return type of the function can be inferred from the type of the property:

function maybeGet<T, N extends keyof T>(t: T | undefined, n: N) {
  return t ? t[n] : undefined;
}

      

with your Test

class

class Test {
  a = {
    id: 1,
    name: "test1"
  };
  f = (foo: string) => {
    return new Test();
  };
};

let t = new Test();

      

you can use it like this:

const a = maybeGet(t, 'a');

      

and indeed the type a

is defined as{ id: number; name: string; }

Then you can define a generic type alias describing some function with a return type R

:

type FR<R> = (...args: any[]) => R;

      

and then define a generic function that will take another function and call it if it is not null. The return type of a function is inferred from the return type of its argument:

function maybeApplyFunction<R>(f: FR<R> | undefined, ...args: any[]) {
  return f ? f(...args) : undefined; 
}

const r = maybeApplyFunction(t.f, 'foo'); // r has type 'Test'

      

You can combine the two together explicitly, no problem

const tf = maybeApplyFunction(maybeGet(t, 'f'), 'foo'); 
// tf has type `Test`

      



The problem is to combine the two into one common operation.

Using a mapped type, you can try to define a type alias for an object that has a function returning R

as a property

type FM<N extends string, R> = {[n in N]: FR<R>};

      

and write a generic function using

function maybeApplyMemberFunction<N extends string, R>(o: FM<N, R>, n: N, ...args: any[]) {
  return o ? o[n](...args) : undefined;
}

      

and it will work even in some cases

class T1 { f() { return new T1() } };

const b = maybeApplyMemberFunction(new T1(), 'f');
// b has type T1

      

however, it won't work for your test, because TypeScript will use keyof T

like for some reason N

and insist that all properties in Test

must be callable:

const tm = maybeApplyMemberFunction(t, 'f');

// Argument of type 'Test' is not assignable to parameter of type 
// 'FM<"a" | "f", Test>'.
  // Types of property 'a' are incompatible.
    // Type '{ id: number; name: string; }' is not assignable to type 'FR<Test>'.
      //  Type '{ id: number; name: string; }' provides no match for the signature
      // '(...args: any[]): Test'.

      

playground code

If you restrict to the N

exact type of the literal, it works:

function maybeApplyMemberFunctionF<N extends 'f', R>(o: FM<N, R>, n: N, ...args: any[]) {
  return o ? o[n](args) : undefined;
}
const t1 = maybeApplyMemberFunctionF(t, 'f');

      

Unfortunately, TypeScript has no way of specifying what N

should be an exact literal of the argument type N

.

0


source







All Articles