Antoine
Antoine

Reputation: 14094

How to type for an optional transform

I think code is the best way to explain what I'm trying to do:

interface Input { in: string }
interface Output { out: string }

function doStuff(input: Input): Output {
    return { out: input.in };
}

function f<Out>(input: Input, transform?: (data: Output) => Out) /* result-type ? */ {
    const output = doStuff(input);
    if(transform) return transform(output);
    else return output;
}
f({in: 'simple'}).out == 'simple';
f({in: 'with transform'}, (data: Output) => data.out) == 'with transform';

I'm struggling to get the return type of the function f right. Basically I want f to return Output when transform is undefined and the result type of transform when it is given.

I tried using different declarations for both cases, and conditional types, but there always is some issue. Can this be modeled with typescript ?

Upvotes: 1

Views: 257

Answers (2)

jsejcksn
jsejcksn

Reputation: 33796

I tried using different declarations for both cases, and conditional types, but there always is some issue. Can this be modeled with typescript?

By "different declarations", I presume that you mean function overloads (which work for your case).

This cannot currently be modeled using conditional types, because TypeScript is not yet capable of inferring type information from optional parameters which are constrained generics. But if it could, your conditional version would look something like this (doesn't work!):

TS Playground

type Transform<I, O> = (data: I) => O;
type Input<T> = { in: T };
type Output<T> = { out: T };

function doStuff <T>(input: Input<T>): Output<T> {
  return { out: input.in };
}

function f <T, Fn extends Transform<Output<T>, any>>(
  input: Input<T>,
  ...[transform]: [] | [transform: Fn]
): typeof transform extends undefined ? Output<T> : ReturnType<Fn> {
  const output = doStuff(input);
  return transform ? transform(output) : output;
}

f({in: 'simple'}).out === 'simple'; // any (incorrectly inferred from the generic constraint)
f({in: 'with transform'}, data => data.out) === 'with transform'; // string (correctly inferred)

Upvotes: 2

tenshi
tenshi

Reputation: 26344

Two overloads do the trick:

function f(input: Input): Output;
function f<T extends (data: Output) => unknown>(input: Input, transform: T): ReturnType<T>;
function f<T extends ((data: Output) => unknown) | undefined>(input: Input, transform?: T)  {
    const output = doStuff(input);
    if(transform) return transform(output);
    else return output;
}

Playground

Upvotes: 1

Related Questions