Сообщение об ошибке, которое вы получаете из своих примеров, вызвано тем фактом, что аргументы типа могут быть явно указаны при вызове функции, например:
fooB<number>(42, (i) => i);
с учетом fooB
:
const fooB = <R1>(value: number = 42, bar: <R0>(val: number) => R0) => {
return bar<R1>(value);
};
, что здесь происходит, так это то, что внутренне bar
можно вызывать с любым другим типом, кроме R1
. Если это так, то нет гарантии, что number
(тип value
) является типом, совместимым с R0
.
Проблема возникает, когда мы делаем, например:
fooB<21>(42, (i) => i);
Об этом вызове мы можем сказать: R1
создается с / as 21
.
Здесь 42 extends number
, как предполагается с учетом типа value
, но number extends 21
ЛОЖЬ. Другими словами: 42
и 21
- это разные подтипы типа number
. Поэтому возвращаемое значение i
не доступно ни для R1
, ни R0
. Аналогичная проблема возникает с foo0B
и R0
.
Уже есть хорошее покрытие о том, почему появляется это сообщение об ошибке.
Исправление
Если я правильно понимаю, вам нужна функция, которая:
- Принимает аргумент типа
T
- Опционально принимает функцию
fn
, которая принимает аргумент введите T
и возвращает значение типа U
- Вызывает
fn
с первым аргументом, а затем возвращает результат (U
), если задано fn
, или возвращает T
в противном случае (идентичность).
Самый простой способ написать это примерно так:
function foo<T, U>(value: T, fn: (val: T) => T | U = i => i): T | U {
return fn(value);
}
Но здесь и foo
, и fn
действительно могут возвращать только тип объединения T | U
, поскольку для обоих случаев имеется только одна подпись
Если вы хотите ветвление типов в зависимости от того, передан ли обратный вызов (я предполагаю, что это по умолчанию I => I
) вместо этого вы можете использовать перегрузку функции . По сути, это все равно, что сказать «У меня есть следующие 2 подписи для foo
»:
function foo<T>(value: T): T;
function foo<T, U>(value: T, fn: (val: T) => U): U;
function foo<T>(value: T, fn = (i: T) => i) {
return fn(value);
}
Первая подпись функции предназначена для случаев, когда обратный вызов не передан: она принимает T
и просто возвращает T
. В этом смысле foo
подобен функции идентификации.
Вторая сигнатура принимает обратный вызов fn
. Если аргументы типа явно не даны для вызова foo
, U
будет выведен из типа fn
. В противном случае тип fn
будет проверен на соответствие данному типу для U
.
Тогда у вас будет реализация функции. Он может учитывать обе подписи и делает это, используя функцию идентификации (значение по умолчанию), чтобы соответствовать первой подписи, или заданную функцию, чтобы соответствовать второй. Эта сигнатура реализации только явно указывает типы, которые являются постоянными для всех перегрузок, поэтому указывается только T
. Перегрузки сделают все остальное, определив тип возврата для каждого вызова.
Вот полная игровая площадка TS с использованием следующих примеров:
// Without callback
const a: number = foo(42); // Fine
const b: string = foo("I'm a string"); // Fine
const c: string = foo(42); // ERROR: type '42' is not assignable to type 'string'
// With callback
const d: number = foo('42', parseInt); // Ok
const e: string = foo(42, (x: number) => x.toString()); // Ok
const f: string = foo(42, (x: number) => x + 1); // ERROR: type 'number' is not assignable to type 'string'
// With explicit types
const g: number = foo<string, number>('42', parseInt); // Ok
const h: number = foo<string, number>('42', (x) => x); // ERROR: type 'string' is not assignable to type 'number'