Попытка использовать условные типы Typescript, чтобы сузить возможные комбинации значений, которые можно ожидать

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

В конкретном примере я хочу, чтобы 2-й параметр был null, если параметр 1sr является примитивным типом, и я хочу, чтобы он был Set (объектов), если 1-й является объектом.

Предыстория для тех, кто хочет знать, почему я хочу это сделать: это оптимизация для обнаружения круга рекурсивной stringify функцией. Клонирование набора seenObjects для разных ветвей, чтобы разрешить дублирование между ветвями, которые на самом деле не образуют круг, необходимо только в том случае, если объект является объектом.

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

Однако внутри функции условный тип, по-видимому, не имеет никакого эффекта. TS жалуется на то, что 2-й параметр, возможно, равен null, несмотря на проверку 1-го параметра, и эта проверка в сочетании с условный тип - если он там использовался - ограничивает тип 2-го параметра до Set, без null.

Код

Ссылка на TS PlayGround (необходимо включить strictNullChecks)

function isObject (thing: unknown): thing is Record<string, any> {
    return typeof thing === 'object' && thing !== null;
}

function stringify<T extends unknown>(
    obj: T,
    seenObjects: T extends Record<string, any> ? Set<Record<string, any>> : null
): string {
    if (isObject(obj)) {
        seenObjects.add(obj);  // ERROR (bad)
    }

    return 'The End';
}

// No errors (good)
stringify(42, null);
stringify([], new Set());

// ERRORS (good)
stringify([], null);
stringify(42, new Set());

Вопрос

Я мог бы просто добавить дополнительные проверки уточнения типа и покончить с этим.

Однако мне любопытно, знает ли кто-нибудь способ добиться желаемого результата без добавления кода. В моем фактическом коде значение 2-го параметра жестко запрограммировано в зависимости от 1-го, поэтому мне кажется уместным решить эту проблему с помощью статического анализа типов, а не с помощью дополнительного кода, полностью ненужного запуска во время выполнения.


PS: Интересно, что, поскольку я просто переключаю эту кодовую базу с Flow на TypeScript, в Flow я мог «взломать» Flow, дав ему код с комментариями, например проверки уточнения типов в /*: …. */, которые Flow интерпретировал бы как «живые». "код. Так что я мог бы успокоить Flow, предоставив ему код, который он хочет видеть для проверки типа, фактически не помещая его в среду выполнения, потому что он находится в комментарии.


Обновление: ошибка ТС?

Когда я меняю условие if с проверки obj на if (seenObjects !== null), ошибка на seenObjects ("может быть нулевой") по-прежнему остается! Несмотря на то, что этот код теперь стоит за явной проверкой null?


person Li Hang    schedule 25.04.2019    source источник
comment
Об обновлении в конце: не уверен, что это можно назвать ошибкой, может быть, скорее отсутствующей функцией. Вы должны назначить массив const, а затем добавить as const в конце. Таким образом, TS рассматривает массив как неизменяемый и принимает типы значений такими, какие они есть, без обобщения. Тогда типы будут выводиться правильно.   -  person Mörre    schedule 26.04.2019
comment
Ограничение компилятора, влияющее на ваше обновление, вероятно, связано с тем фактом, что анализ потока управления не ограничивает универсальные типы и что условные типы нелегко анализировать на предмет присваиваемости. Над последней проблемой ведется работа, но я не знаю, будет ли она сделать это в компиляторе в ближайшее время.   -  person jcalz    schedule 26.04.2019


Ответы (1)


Элемент управления TypeScript потоковый анализ не понимает, когда разные переменные имеют коррелирующие типы. Если вы сузите тип одной переменной, это не повлияет на тип другой переменной. Кроме того, TypeScript сложно выполнять анализ типов для неразрешенных условных типов, поэтому, даже если вы изменили свой код для анализа типа seenObjects вместо obj, вы, вероятно, все равно получите некоторые ошибки типов.

И вы, похоже, не заинтересованы в добавлении дополнительных проверок во время выполнения, чтобы убедить компилятор в том, что все безопасно.

Хорошо: если вы сомневаетесь и когда вы умнее компилятора, вы всегда можете использовать утверждение типа, чтобы успокоить компилятор без изменения сгенерированного JavaScript:

function stringify<T extends unknown>(
    obj: T,
    seenObjects: T extends Record<string, any> ? Set<Record<string, any>> : null
): string {
    if (isObject(obj)) {
        (seenObjects as Set<Record<string,any>>).add(obj); // type assertion 
    }    
    return 'The End';
}

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

person jcalz    schedule 26.04.2019
comment
Анализ потока управления TypeScript не понимает, когда разные переменные имеют коррелирующие типы. -- Но это так, данный пример работает наполовину. При вызове функции эта информация действительно используется TS. Если вы действительно очень конкретно не имеете в виду переменную, то есть исключая параметры to функции. Но тогда вы просто пересказываете то, что здесь можно наблюдать :-) - person Mörre; 26.04.2019
comment
Я имею в виду анализ потока управления в отличие от вывода параметра универсального типа. Половина, которая не работает, - это защита типа obj, чтобы повлиять на тип seenObjects внутри реализации функции. Половина, которая действительно работает, заключается в том, что на сайтах вызова функций компилятор выводит тип T из первого параметра, который ограничивает сигнатуру всей функции. Вывод универсального типа и анализ потока управления, я думаю, имеют схожие эффекты, но это разные процессы компиляции. - person jcalz; 26.04.2019