Typescript «Условные» типы - PullRequest
0 голосов
/ 02 декабря 2018

У меня есть простая функция, которая в принципе выполняет простую работу, но плохое описание типа, требующее подтверждения типов почти каждый раз, когда я манипулирую данными.

Функция:

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;
    }
};

Указанные типы:

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;

В основном он имеет 2 переменные source и encode, каждая из которых может иметь один из двух действующих типов, что приводит к 4 состояниям.source - это либо элемент данных, либо функция, которая извлекает элемент данных.encode - это либо undefined, либо функция, которая преобразует «результат» source.В конце концов, эта комбинация должна привести к значению (относительно) простого типа, Encoded.

Я пытался улучшить определение типа несколькими способами, но, похоже, не могу избежать необходимостиутверждения типа.Эти попытки также были направлены как на углубление моего понимания системы типов, так и на очистку фактических определений.Тем не менее, мне кажется, что я должен быть способен задавать типы достаточно плотно, чтобы избежать утверждений типа, и я хотел бы понять, как.

Моя первая попытка с использованием союзов, похоже, нена самом деле вообще улучшить определение типа:

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'
    }
};

Затем я попытался использовать фактические условные типы, но также не получил ничего:

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'
    }
};

Я не уверен, куда еще идтиздесь.

Согласно предложению Инго Бюрка (ниже), я пробовал перегрузки, и они решили первоначальные проблемы, но представили новую, которая меня озадачивает:

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;
    }
}

Если я добавлю мое текущее (универсальное) определение в качестве значения по умолчанию, вышеуказанная ошибка исчезнет, ​​но утверждения типа снова потребуются.

1 Ответ

0 голосов
/ 04 декабря 2018

Вот как вы можете сделать это с перегрузками.Обратите внимание, что фактическое тело функции нетипизировано, я не смог найти хороший способ заставить это работать (и я не уверен, что это вообще возможно).Но вызовы функций набираются правильно.

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;
}

Это проходит следующие тесты типа:

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...