Более строгое утверждение типа в Typescript

Итак, скажем, у меня есть

declare function doSomething(...args: any[]): any

interface Example {
    a: number
    b: number
}

doSomething({a: 2, b: 1, c: 10} as Example)

Это не сообщает об ошибке, поскольку этот объект расширяет Example, и typescript доволен, поэтому я использую уродливую функцию идентификации:

function cast<T>(arg: T) {
    return arg
}

doSomething(cast<Example>({a: 2, b: 1, c: 10})) // yay, error

Меня чертовски раздражает, что мне нужно на самом деле вызвать функцию %^#% no-op только для того, чтобы выполнить правильное приведение типа. Я заканчиваю тем, что объявляю его в каждом файле, который в нем нуждается, просто чтобы дать компилятору js больше шансов его оптимизировать.

Есть ли какая-то магия ts, о которой я не знаю, которая может избежать вызова функции?

И да, я знаю, что могу сделать это:

const x: Example = {a: 2, b: 1, c: 10}
doSomething(x)

и это:

declare function doSomething(arg: Example): any

тут действительно не в этом дело. Рассмотрим следующую лямбду:

const example = (i: number, j: number, k: number) => cast<Example>({a: 1, b: 2, c: 3})

чтобы правильно установить тип, не приводя к функции идентификации, мне нужно было бы написать

const example: (i: number, j: number, k: number) => Example = (i, j, k) => ({a: 1, b: 2, c: 3})

который не очень СУХОЙ

И да, я могу просто написать

function example (i: number, j: number, k: number): Example {
    return {a: 1, b: 2, c: 3}
}

опять же не суть

// редактировать

поскольку @thedude просто поразил меня в комментарии синтаксисом ввода лямбда-возврата, о котором я не знал, еще один пример, где я использую это приведение

declare function doSomethingWithArray(arg: Example[]): void

doSomethingWithArray(cast<(Example | boolean)[]>([
    {a: 1, b: 2},
    false,
    {a: 1, b: 2, c: 3}
]).filter(x => x) as Example[])

// редактируем 2

как кажется, я плохо объясняю, что я хочу, другой пример: эта общая функция решает проблему примера фильтра

function filterFalse<T>(x: (T | false)[]) {
    return x.filter(x => x) as Exclude<T, false>[]
}

doSomethingWithArray(filterFalse<Example>([
    {a: 1, b: 2},
    false,
    {a: 1, b: 2, c: 3} // error
]))

но требует определения специализированной функции только для этой конкретной задачи. Я спрашиваю, есть ли общий способ принудительной проверки строгого типа для литерала времени компиляции без вызова функции. Итак, именно то, что делает cast<T>, но без бессмысленного вызова в выводе js.


person Otis Vallone    schedule 11.07.2021    source источник
comment
вы также можете написать: const example = (i: number, j: number, k: number): Example => ({a: 1, b: 2, c: 3})   -  person thedude    schedule 11.07.2021
comment
Что не так с последним фрагментом? Почему это не главное? Если вы пишете код, который не использует any, то вам очень редко нужно выполнять подобное приведение типов.   -  person Alex Wayne    schedule 12.07.2021
comment
дело в том, что я хотел объявление лямбда, а не объявление функции, которое действительно решено с помощью того, что @thedude написал в комментарии выше, см. Редактировать для другого примера   -  person Otis Vallone    schedule 12.07.2021
comment
@OtisVallone В новом фрагменте вам все еще не нужна функция cast. Функция filter может работать с любым массивом, поэтому вы можете передать ей массив смешанных логических значений и Example объектов. Typescript уже сделает вывод, что тип этого массива — (boolean | Example)[].   -  person siride    schedule 12.07.2021
comment
Фильтр @siride выводит массив объединения литеральных объектов (и логических), а не (логический | Пример) []. К тому же по какой-то неведомой мне причине даже этот doSomethingWithArray([{a: 1, b: 2, c: 3}].filter(x => x)) не выходит из строя на c, а без фильтра выходит.   -  person Otis Vallone    schedule 12.07.2021
comment
@OtisVallone Есть много вариантов, но один из них, который я только что протестировал, заключался в использовании as (boolean | Example)[] в массиве перед вызовом фильтра. Вам никогда не нужна функция cast. Вам просто нужно использовать as или следовать этому коду, чтобы сообщить TypeScript, что filter является защитой типа: stackoverflow.com/questions/43010737/.   -  person siride    schedule 12.07.2021
comment
@siride Если есть способ сделать это таким образом, который выдает ошибку c, не существующую в Example, пожалуйста, покажите мне, как это сделать, я не понимаю, как этого добиться ни с утверждениями типа, ни с защитой типа, ни без определения специализированной функции только для это   -  person Otis Vallone    schedule 12.07.2021
comment
@OtisVallone: ​​см. обновление ответа Алекса Уэйна в конце.   -  person siride    schedule 12.07.2021
comment
@siride Я хочу, чтобы компилятор строго проверял типы ключей объектов, в этом весь смысл. Я знаю, что могу просто привести результат к тысяче разных способов, но это не меняет того факта, что компилятор по-прежнему будет проверять ключи объекта более слабым способом, чем обычно (принимая ключи, которые не определены в интерфейсе). Ответ Алекса Уэйна решает эту проблему, говоря, что это функция.   -  person Otis Vallone    schedule 12.07.2021
comment
@OtisVallone Похоже, ваша основная проблема заключается в том, что filter() может принимать массив чего угодно, в принципе. Прежде чем вы приступите к фильтрации, вы должны убедиться, что массив уже ограничен. See this example: typescriptlang.org/play?#code/.   -  person siride    schedule 13.07.2021


Ответы (1)


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


Давайте начнем здесь, что прекрасно и так.

doSomething({a: 2, b: 1, c: 10} as Example)

Ключевое слово as по-прежнему обычно безопасно для типов. Это позволяет вам привести значение к супертипу. В этом случае { a: number, b: number } является супертипом { a: number, b: number, c: number }.

Например:

const value = { a: 1, b: 2, c: 2 }
const exampleValue: { a: number, b: number } = value // works!

И в вашем первом примере, если вы пропустите обязательное свойство, у вас будет ошибка типа:

doSomething({b: 1, c: 10} as Example)
// Property 'a' is missing in type '{ b: number; c: number; }'
// but required in type 'Example'.(2352)

Так почему же это дает ошибку?

cast<Example>({a: 1, b: 2, c: 3}) // error

Потому что, когда вы создаете значение и присваиваете его супертипу, вы ссылаетесь только на это значение, относящееся к этому супертипу. Это означает, что любые другие свойства, которые могут существовать, будут недоступны. Example не имеет свойства c. Итак, typescript предполагает, что это ошибка, и это правильно.

Но в реальном коде, если вы передадите объект с a, b и c функции, которая ожидает только a и b, то ничего плохого не произойдет. Вы удовлетворили ограничения, так что проблем нет.


В этом фрагменте:

doSomething(cast<Example>({a: 2, b: 1, c: 10})) // yay, error

Актерский состав бессмысленный. Если doSomething заботится о типе, который он получает, он должен объявить это в своих аргументах. А если все равно, то и приведение не нужно вообще.


const x: Example = {a: 2, b: 1, c: 10}
doSomething(x)

Это в основном то же самое, что и as Example сверху.


declare function doSomething(arg: Example): any

Это правильно, и именно то, что вы должны делать. Если функция принимает определенный тип, позвольте ей применять его самостоятельно. Вам не нужно ничего разыгрывать, если вы назначаете его any, так как это приведение все равно будет потеряно.


const example = (i: number, j: number, k: number) =>
  cast<Example>({a: 1, b: 2, c: 3})

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

const example(i: number, j: number, k: number): Example =>
  ({ a: i, b: j, c: k }) // error, as expected

Чтобы правильно установить тип, не приводя к функции идентификации, мне нужно было бы написать:

const example: (i: number, j: number, k: number) => Example = (i, j, k) => ({a: 1, b: 2, c: 3})

Неверно, см. предыдущий пример. Одна функция, тип применяется, как и ожидалось.


Наконец:

function example (i: number, j: number, k: number): Example {
    return {a: 1, b: 2, c: 3} // error as expected.
}

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


TLDR:

Ни один из ваших фрагментов кода не должен требовать функции cast<T>(). Я думаю, что вам лучше всего устранить все вхождения any, а затем просто позволить Typescript применять типы. Вот для чего он нужен. Компилятор довольно хорош и обычно не нуждается в такой помощи.


Фильтрация

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

Тогда вы можете сделать что-то вроде:

interface Example {
    a: number
    b: number
}

declare function doSomethingWithArray(arg: Example[]): void

doSomethingWithArray([
    {a: 1, b: 2},
    false,
    {a: 1, b: 2, c: 3} // no error because of perfectly safe casting to supertype
].filter((x): x is Example => typeof x !== 'boolean'))

See playground

person Alex Wayne    schedule 11.07.2021
comment
первый фрагмент, чтобы объяснить, чего я хочу, вторые два, где объяснить, чего я не хочу, лямбда была единственным реальным примером, где я ее использую. Я не знал о синтаксисе лямбда, где вы можете напрямую указать тип возвращаемого значения, который решает пример лямбда, я добавил еще один пример, где я использую этот вид приведения. - person Otis Vallone; 12.07.2021
comment
Смотрите в конце моего ответа обновленную информацию о фильтрации. Вы можете использовать функцию предиката типа, чтобы сообщить typescript, что ваша операция фильтрации изменяет тип массива. - person Alex Wayne; 12.07.2021
comment
Послушайте, я слишком много раз делал опечатки в необязательных полях объекта, чтобы согласиться с вами, что это совершенно безопасно. По сути, вы предполагаете, что каждый интерфейс со всеми необязательными полями так же хорош, как Record<string, any> (ну, не совсем потому, что он по-прежнему будет проверять типы значений существующих ключей, но я надеюсь, что вы поняли картину). Кроме того, если это не ошибка, то почему это ошибка, когда вы просто передаете необработанный массив, а не отфильтрованную версию? Значение можно безопасно привести к типу параметра как к супертипу, но компилятор все равно сообщает об ошибке (как и должно быть). - person Otis Vallone; 12.07.2021