Почему Parameters‹Func› не расширяет unknown[] (Array‹unknown›) в строгом режиме ts

Название в значительной степени говорит само за себя. У меня есть этот код:

  type testNoArgsF = () => number;
  type testArgsF = (arg1: boolean, arg2: string) => number;
  type unknownArgsF = (...args: unknown[]) => number;
  type anyArgsF = (...args: any[]) => number;

  type testII = testArgsF extends anyArgsF ? true : false; // true
  type testIII = Parameters<testArgsF> extends Parameters<unknownArgsF>
    ? true
    : false; // true

  // unexpected:
  type testIV = testArgsF extends unknownArgsF ? true : false; // false <- why?
  // even though:
  type testV = testNoArgsF extends unknownArgsF ? true : false; // true

Он написан на машинописном языке (версия 3.8), и у меня включен строгий режим. Неожиданным результатом является то, что тестовая функция не расширяет тип функции с расширенными аргументами unknown[], но если вы просто проверите параметры, которые они делают, расширяют unknown[]. Поскольку возвращаемый тип всегда является числом, я не понимаю, что еще может быть другим, чтобы фальсифицировать оператор extends.

Другие примечания:

  • Оператор расширения верен, только если ваша тестовая функция имеет 0 аргументов.
  • Это поведение не видно, если вы отключите строгий режим.

person Geoff Davids    schedule 01.06.2020    source источник


Ответы (3)


С параметром --strictFunctionTypes компилятора< /a>, параметры типа функции проверяются контравариантно. «Контравариант» означает, что отношение подтипа функции изменяется в направлении, противоположном отношению параметров функции. Так что если A extends B, то (x: B)=>void extends (x: A)=>void а не наоборот.

Это проблема безопасности типов из-за природы «заменяемости» в TypeScript, также известной как поведенческий подтип. Если A extends B истинно, вы сможете использовать A как B. Если не можете, то A extends B неверно.

Если вы отключите --strict, компилятор будет использовать поведение до TS-2.6 для проверки параметров функции бивариантно, что небезопасно, но разрешено из соображений производительности. Это может быть не по теме здесь, но вы можете прочитать больше об этом в разделе FAQ по TypeScript для "Почему параметры функции бивариантны?"


В любом случае, если вам нужен тип функции, который принимает любое количество параметров unknown, вы не можете безопасно использовать функцию, которая имеет только определенный подтип unknown. Наблюдать:

const t: testArgsF = (b, s) => (b ? s.trim() : s).length
const u: unknownArgsF = t; // error!

u(1, 2, 3); // explosion at runtime! s.trim is not a function

Если бы testArgsF extends unknownArgsF было истинным, то вы могли бы присвоить t указанному выше u без ошибок, что немедленно привело бы к ошибкам времени выполнения, когда u счастливо принимает аргумент, отличный от string секунды.

Вы видите, что единственный безопасный способ создать подтип/реализовать тип функции — это принять аргументы, которые являются такими же или более широкими, чем те, которые ожидаются супертипом/сигнатурой вызова. Вот почему --strictFunctionTypes был введен в язык.


Если вы измените unknown на any (используя anyArgsF вместо unknownArgsF), то компилятор не будет жаловаться, потому что any намеренно неправильный в TypeScript. Тип any считается назначаемым как в, так и из любого другого типа; это небезопасно, потому что, например, string extends any и any extends number оба истинны, а string extends number ложны. Таким образом, описанный выше принцип замещения не применяется, когда используется any. Аннотирование значения как типа any эквивалентно ослаблению или отключению проверки типа для этого значения. Это не спасет вас от ошибки времени выполнения; он просто заглушает ошибку компилятора:

const a: anyArgsF = t; // okay, type checking with any is disabled/loosened
a(1, 2, 3); // same explosion at runtime!

В случае, когда testNoArgsF extends unknownArgsF истинно, это также является следствием взаимозаменяемости. Вы можете использовать функцию, которая не принимает аргументов, как если бы это была функция любого типа, поскольку она (обычно) в конечном итоге игнорирует любые переданные ей аргументы:

const n: testNoArgsF = () => 1;
const u2: unknownArgsF = n; // okay
u2(1, 2, 3); // okay at runtime, since `n` ignores its arguments

Это объясняется в записи FAQ по TypeScript "Почему функции с меньшим количеством параметров можно назначать функциям, которые принимают больше параметров?".


Хорошо, надеюсь, это поможет; удачи!

Playground link to code

person jcalz    schedule 01.06.2020
comment
Я предполагаю, что разницу между типами параметров и типами функций, а также причину, по которой они являются контравариантными, можно понять интуитивно, хотя это рассуждение: когда вы указываете тип функции, вы говорите, что вы даете, тогда как, когда вы заявляете тип параметра вы говорите, что получаете (и вы/ваша функция должны обрабатывать все возможные варианты). - person Geoff Davids; 30.06.2020
comment
Означает ли это, что единственный способ указать универсальный тип шаблона функции — (...args: any[]) => any? Просто я читал, что вы всегда должны стараться избегать any, если это возможно, поэтому вместо этого пытался использовать unknown. - person Geoff Davids; 30.06.2020
comment
Причина не использовать any заключается в том, что он отключает некоторые проверки типов. Но большинство других способов, которыми вы можете попытаться описать какую-либо функцию, тоже делают это. Function и (...args: never[])=>unknown будут соответствовать любой функции, но допускают неправильные вызовы (see). Так что это зависит от вашего варианта использования, на самом деле. - person jcalz; 30.06.2020
comment
Мой вариант использования заключался в определении универсального типа функции Func для использования в качестве требования базового универсального типа в функции const apply = <F extends Func>(func: F, ...params: Parameters<F>): ReturnType<F> => func(...params);. Это было необходимо для вызова типизированной переменной объединения функций, даже если были предоставлены соответствующие параметры (это было в модификаторе для библиотеки подобных функций). Но похоже, что здесь действительно нужен аспект принуждения/небезопасности типа any. - person Geoff Davids; 30.06.2020

Я постараюсь выразиться проще: «расширяет» -> «совместимо» -> «можно использовать вместо»

Можно ли использовать (arg1: boolean, arg2: string) => number;
вместо (...args: unknown[]) => number?

Нет, потому что последний может обрабатывать вызов без аргументов, но первый может дать сбой во время выполнения (например, попытается получить доступ к свойствам аргументов).

Подробнее о совместимости функций здесь

person Aleksey L.    schedule 01.06.2020

type testIV = testArgsF extends unknownArgsF ? true : false; // false 
type testIVa = unknownArgsF extends testArgsF  ? true : false; // true!

// because
type testIII = Parameters<testArgsF> extends Parameters<unknownArgsF>
    ? true
    : false; // true

Расширяется на самом деле означает «назначается». И f(p:A)=> присваивается f(p:B)=>, если B присваивается A. Я думаю, это называется «контравариантным».

// Imagine your function expects callback that has some type as a paramter:

var f = (cb: ({ a, b }: { a: number, b: number }) => void) => { cb({ a:1, b: 1}); };

// you can safely call it giving it the function that accepts parameter 
// that the original parameter is assignable to but not the other way around
// because that's the way to guarantee that when our function f calls the callback
// it will provide sufficient amount of data
f(({ a }: { a: number }) => { })


var x = { a: 1, b: 1 };
var y = { a: 1 }

x = y; // error because x would have no b property after that assignment
y = x; // works because y has all it needs after that assignment and it doesn't matter it has property b too

// in other words:
type F = { a: number } extends { a: number, b: number } ? true : false; // false
type T = { a: number, b: number } extends { a: number } ? true : false; // true

// read 'extends' as 'is assignable to'
person Kamil Szot    schedule 01.06.2020
comment
Я не совсем понимаю ваш четырехстрочный комментарий во втором блоке кода... не могу понять, веду ли я себя глупо или это немного неясно/запутанно! Не волнуйтесь, я думаю, что понял суть - person Geoff Davids; 30.06.2020