1

I have a simple function that has a simple job, in principal, but a poor type description requiring type assertions nearly every time I manipulate data.

The function:

const fetch_and_encode = <T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<T | E> | T | E,
    encode?: Encoder<T, E, C>,
    context?: C
}): E => {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = (source as Fetcher<T | E>)();
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded as T, context);
    } else {
        return decoded as E;
    }
};

The referenced types:

export type Encoded = number | string | ArrayBuffer // | Array<Encoded> | Map<string, Encoded>
export type Fetcher<T> = () => T;
export type Encoder<T, E extends Encoded, C> = (decoded: T, context?: C) => E;

It basically has 2 variables, source and encode, that can each have one of two effective types, leading to 4 states. source is either a datum or function that retrieves a datum. encode is either undefined or a function that transforms the 'result' of source. In the end, this combination must result in a value of (relatively) simple type, Encoded.

I've tried to improve the type definition a couple different ways, but can't seem to avoid needing the type assertions. These attempts have also been as much about deepening my understanding of the type system as cleaning up the actual definitions. That said, it 'feels' like I should be able to specify the types tightly enough to avoid the type assertions and I'd like to understand how.

My first attempt, using unions, didn't seem to actually improve the type definition at all:

const fetch_and_encode = <T, E extends Encoded, C>( {source, encode, context}: {
    source: Fetcher<T>;
    encode: Encoder<T, E, C>;
    context?: C;
} | {
    source: Exclude<T, Function>; // T could still be a subtype of Function
    encode: Encoder<T, E, C>;
    context?: C;
} | {
    source: Fetcher<E>;
    encode: undefined;
    context?: any;
} | {
    source: E;
    encode: undefined;
    context?: any;
}): E => {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = (source as Fetcher<T | E>)();
        // decoded = source(); // Cannot invoke an expression whose type lacks a call signature.  Type 'Fetcher<T> |
        //                     // Fetcher<E> | (Exclude<T, Function> & Function)' has no compatible call signatures.
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded as T, context);
        // return encode(decoded, context); // Argument of type 'T | E' is not assignable to parameter of type 'T'
    } else {
        return decoded as E;
        // return decoded; // Type 'T | E' is not assignable to type 'E'
    }
};

Then I attempted to use actual conditional types, and also got nowhere:

const fetch_and_encode = <T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<T | E> | T | E,
    encode: Encoder<T, E, C> | undefined,
    context: C | undefined
} extends { source: infer S, encode: infer N, context?: C }
    ? S extends Function // Ensure S isn't a Function if it also isn't a Fetcher
        ? S extends Fetcher<T | E>
            ? N extends undefined
                ? { source: Fetcher<E>; encode: undefined; context?: any; }
                : { source: Fetcher<T>; encode: Encoder<T, E, C>; context?: C; }
            : never
        : N extends undefined
            ? { source: E; encode: undefined; context?: any; }
            : { source: T; encode: Encoder<T, E, C>; context?: C; }
    : never
): E => {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = (source as Fetcher<T | E>)();
        // decoded = source(); // Cannot invoke an expression whose type lacks a call signature.  Type 'Fetcher<T> |
        //                     // Fetcher<E> | (T & Function)' has no compatible call signatures.
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded as T, context);
        // return encode(decoded, context); // Argument of type 'T | E' is not assignable to parameter of type 'T'
    } else {
        return decoded as E;
        // return decoded; // Type 'T | E' is not assignable to type 'E'
    }
};

I'm not sure where else to go from here.

Per the suggestion of Ingo Bürk (below), I tried overloads, and they solved the original issues, but presented a new one that perplexes me:

function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    //   ^^^^^^^^^^^^^^^^ Overload signature is not compatible with function implementation
    source: E;
    encode: undefined;
    context?: any;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<E>;
    encode: undefined;
    context?: any;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: Fetcher<T>;
    encode: Encoder<T, E, C>;
    context?: C;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
    source: Exclude<T, Function>; // T could still be a subtype of Function
    encode: Encoder<T, E, C>;
    context?: C;
}): E {
    let decoded;
    if ( typeof source === 'function' ) {
        decoded = source();
    } else {
        decoded = source;
    }
    if ( typeof encode === 'function' ) {
        return encode(decoded, context);
    } else {
        return decoded;
    }
}

If I add in my current (generic) definition as the default, the above error goes away, but type assertions are once again required.

6
  • It might be easiest to just overload the signature: typescriptlang.org/docs/handbook/functions.html#overloads Commented Dec 2, 2018 at 8:19
  • I hadn't considered that, yet. I'll give it a try (and pray it won't be too verbose ;-) Commented Dec 2, 2018 at 8:23
  • The source argument can probably be handled without an overload, but for the encode case I think it might be easier. I'm just on mobile so I can't play around with it myself right now though. I'm sure Titian will drop in soon and give you one of his amazing answers. Commented Dec 2, 2018 at 8:28
  • Overloads... altered the problem. I'll add details. Commented Dec 2, 2018 at 8:42
  • Another idea would be to make the encoder an optional parameter that defaults to the identity function. That makes typing it trivial, though of course technically doesn't answer the question. Commented Dec 3, 2018 at 22:42

1 Answer 1

1

Here's how you can do it with overloads. Note that the actual function body is untyped, I couldn't find a good way to get that to work (and am not sure it's even possible). But the functions calls are typed correctly.

function isFetcher<T>(obj: T | Fetcher<T>): obj is Fetcher<T> {
  return typeof obj === "function";
}

function fetchAndEncode<A extends Encoded>(source: A | Fetcher<A>): A;
function fetchAndEncode<A, B extends Encoded, C>(source: A | Fetcher<A>, encode: Encoder<A, B, C>, context?: C): B;
function fetchAndEncode(source: any, encode?: any, context?: any) {
  const datum = isFetcher(source) ? source() : source;
  return encode ? encode(datum, context) : datum;
}

This passes the following type tests:

let numericValue: number;

fetchAndEncode(numericValue); // number
fetchAndEncode(true); // error
fetchAndEncode(numericValue, val => `Hello ${val}`); // string
fetchAndEncode(() => numericValue); // number
fetchAndEncode(() => true); // error
fetchAndEncode(() => numericValue, val => `Hello ${val}`); // string
Sign up to request clarification or add additional context in comments.

4 Comments

Perhaps my model needs refinement, but I explicitly need the function to return B, as that's constrained elsewhere (via Encoded). A is a possibly un-encoded type, if the consumer of this function has provided encode.
@BenjaminRiggs I'm not following, this solution does return B if you have passed an encoder? Can you post a call where it doesn't work?
If an encode is undefined, then A must also extend Encoded, and, from my read, this example doesn't enforce that.
@BenjaminRiggs I see, I completely missed that. I updated my answer.

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.