2

Suppose we have the following function:

function test<S, T>(obj: S, prop: keyof S, mapper?: (value: S[keyof S]) => T): S[keyof S] | T {
  return typeof mapper === 'function'
    ? mapper(obj[prop])
    : obj[prop];
}

If then I use it without the mapper argument, the type of the return value is not deduced properly:

const value = test({ a: 'stringValue' }, 'a'); // value is of type "unknown"

But if I provide an identity function as the third parameter, it is deduced correctly:

const value = test({ a: 'stringValue' }, 'a', x => x); // value is of type "string"

How should the test function be typed so when we don't provide the mapper argument, the return value's type is deduced correctly?

Playground link

2 Answers 2

3

Just use function overloads !

function test<S, T>(obj: S, prop: keyof S): S[keyof S];
function test<S, T>(obj: S, prop: keyof S, mapper: (value: S[keyof S]) => T): T;
function test<S, T>(obj: S, prop: keyof S, mapper?: (value: S[keyof S]) => T): S[keyof S] | T {
    return typeof mapper === 'function'
        ? mapper(obj[prop])
        : obj[prop];
}

const value = test({ a: 'stringValue' }, 'a'); // string

const value2 = test({ a: 'stringValue' }, 'a', x => x);  // string

Playground

Sign up to request clarification or add additional context in comments.

2 Comments

@DMZ - Expanding on Matthieu's answer: The issue with your original code is that without a mapper function, there's no way for TypeScript to infer T, because the only place T exists in the parameter list is in relation to mapper. So T is unknown, so the result of test is unknown. The function overload solution avoids that by defining an overload without mapper that returns S[keyof S] and an overload with mapper that returns T.
I was so close. Thanks, don't know how did I miss that.
1

The usual approach is to help the compiler by moving more information to the generic variables of the function. Does this work for you? (Playground)

function test<
    Obj,
    Prop extends keyof Obj,
    Mapper extends ((value: Obj[Prop]) => any) | undefined
>(
    obj: Obj,
    prop: Prop,
    mapper?: Mapper
): undefined extends Mapper ? Obj[Prop] : ReturnType<NonNullable<Mapper>> {
    return mapper ? mapper(obj[prop]) : obj[prop];
}

const value1 = test({ a: "123" }, "a");

const value2 = test({ a: "123" }, "a", x => parseInt(x));

The optional mapper makes the code harder to write, but I am pretty sure this code can be simplified further.

2 Comments

Interesting approach, this works as well. The "undefined extends Mapper" is kind of confusing though.
You can also write the more natural [Mapper] extends [Function] ? ReturnType<Mapper> : Obj[Prop], but the non-distributive extends may be also confusing for TS newcomers.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.