Typescript: Точные типы

Созданный на 15 дек. 2016  ·  171Комментарии  ·  Источник: microsoft/TypeScript

Это предложение включить синтаксис для точных типов. Похожую функцию можно увидеть в Flow (https://flowtype.org/docs/objects.html#exact-object-types), но я хотел бы предложить ее как функцию, используемую для литералов типов, а не интерфейсов. Конкретный синтаксис, который я предлагаю использовать, - это конвейер (который почти отражает реализацию Flow, но должен окружать оператор типа), поскольку он известен как математический абсолютный синтаксис.

interface User {
  username: string
  email: string
}

const user1: User = { username: 'x', email: 'y', foo: 'z' } //=> Currently errors when `foo` is unknown.
const user2: Exact<User> = { username: 'x', email: 'y', foo: 'z' } //=> Still errors with `foo` unknown.

// Primary use-case is when you're creating a new type from expressions and you'd like the
// language to support you in ensuring no new properties are accidentally being added.
// Especially useful when the assigned together types may come from other parts of the application 
// and the result may be stored somewhere where extra fields are not useful.

const user3: User = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Does not currently error.
const user4: Exact<User> = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Will error as `foo` is unknown.

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

type Foo = Exact<X> | Exact<Y>

type Bar = Exact<{ username: string }>

function insertIntoDb (user: Exact<User>) {}

Заранее извиняюсь, если это дубликат, мне не удалось найти правильные ключевые слова, чтобы найти дубликаты этой функции.

Изменить: этот пост был обновлен, чтобы использовать предпочтительное предложение синтаксиса, упомянутое на https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -267272371, которое включает использование более простого синтаксиса с универсальным типом для включения использования в выражениях.

Awaiting More Feedback Suggestion

Самый полезный комментарий

Мы говорили об этом довольно долго. Попробую подвести итог дискуссии.

Проверка лишнего имущества

Точные типы - это всего лишь способ обнаружить дополнительные свойства. Спрос на точные типы сильно упал, когда мы изначально внедрили проверку избыточных свойств (EPC). EPC был, вероятно, самым большим критическим изменением, которое мы сделали, но оно окупилось; почти сразу мы получили ошибки, когда EPC не обнаружил лишнее свойство.

По большей части, когда людям нужны точные типы, мы предпочли бы исправить это, сделав EPC умнее. Ключевой областью здесь является случай, когда целевой тип является типом объединения - мы хотим просто принять это как исправление ошибки (EPC должен работать здесь, но он еще не реализован).

Необязательные типы

С EPC связана проблема необязательных типов (которые я называю «слабыми»). Скорее всего, все слабые типы захотят быть точными. Нам просто нужно реализовать определение слабого типа (# 7485 / # 3842); Единственным препятствием здесь являются типы пересечений, реализация которых требует дополнительной сложности.

Чей тип точен?

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

На одном конце спектра у вас есть функции, которые буквально выбрасывают исключение (или иным образом делают плохие вещи), если объекту с собственным ключом предоставляется за пределами некоторого фиксированного домена. Таких немного и далеко друг от друга (не могу назвать пример по памяти). Посередине находятся функции, которые молча игнорируют
неизвестные свойства (почти все). А на другом конце у вас есть функции, которые обычно работают со всеми свойствами (например, Object.keys ).

Очевидно, что функции «будет выдавать, если будут предоставлены дополнительные данные» должны быть помечены как принимающие точные типы. А как насчет середины? Люди, скорее всего, не согласятся. Point2D / Point3D - хороший пример - можно разумно сказать, что функция magnitude должна иметь тип (p: exact Point2D) => number чтобы предотвратить передачу Point3D . Но почему я не могу передать объект { x: 3, y: 14, units: 'meters' } этой функции? Здесь на помощь приходит EPC - вы хотите обнаружить это «лишнее» свойство units в тех местах, где оно определенно отбрасывается, но не блокировать вызовы, связанные с псевдонимом.

Нарушение предположений / проблемы с инстанцией

У нас есть несколько основных принципов, которые не позволяют использовать точные типы. Например, предполагается, что тип T & U всегда присваивается T , но это не удается, если T является точным типом. Это проблематично, потому что у вас может быть какая-то универсальная функция, которая использует этот принцип T & U -> T , но вызывает функцию с T созданную с точным типом. Таким образом, у нас нет никакого способа сделать этот звук (на самом деле это не нормально для ошибки при создании экземпляра) - не обязательно блокировщик, но сбивает с толку то, что общая функция более разрешительна, чем созданная вручную версия самой себя!

Также предполагается, что T всегда присваивается T | U , но не очевидно, как применить это правило, если U - точный тип. Можно ли присвоить { s: "hello", n: 3 } { s: string } | Exact<{ n: number }> ? «Да» кажется неправильным ответом, потому что тот, кто ищет n и обнаруживает его, не будет рад увидеть s , но «Нет» также кажется неправильным, потому что мы нарушили базовый T -> T | U правило.

Разное

Что означает function f<T extends Exact<{ n: number }>(p: T) ? :смущенный:

Часто требуются точные типы, тогда как на самом деле вам нужно «автоматически разъединенное» объединение. Другими словами, у вас может быть API, который может принимать { type: "name", firstName: "bob", lastName: "bobson" } или { type: "age", years: 32 } но не хочет принимать { type: "age", years: 32, firstName: 'bob" } потому что произойдет что-то непредсказуемое. «Правильный» тип - это, возможно, { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } но, черт возьми, неприятно печатать. Мы могли бы потенциально подумать о сахаре для создания подобных типов.

Резюме: Необходимые варианты использования

Наш обнадеживающий диагноз заключается в том, что это, помимо относительно небольшого количества действительно закрытых API, является решением проблемы XY . По возможности мы должны использовать EPC для обнаружения «плохих» свойств. Поэтому, если у вас есть проблема, и вы думаете, что точные типы являются правильным решением, опишите исходную проблему здесь, чтобы мы могли составить каталог шаблонов и посмотреть, есть ли другие решения, которые были бы менее инвазивными / запутанными.

Все 171 Комментарий

Я бы предположил, что синтаксис здесь спорен. Так как TypeScript теперь позволяет использовать ведущую трубу для типа объединения.

class B {}

type A = | number | 
B

Компилируется сейчас и эквивалентен type A = number | B благодаря автоматической вставке точки с запятой.

Я думаю, этого не ожидал, если будет введен точный тип.

Не уверен, что это правда, но к вашему сведению https://github.com/Microsoft/TypeScript/issues/7481

Если бы был принят синтаксис {| ... |} , мы могли бы использовать сопоставленные типы, чтобы вы могли писать

type Exact<T> = {|
    [P in keyof T]: P[T]
|}

а затем вы можете написать Exact<User> .

Это, наверное, последнее, чего мне не хватает в Flow по сравнению с TypeScript.

Пример Object.assign особенно хорош. Я понимаю, почему TypeScript ведет себя так, как сегодня, но в большинстве случаев я предпочитаю иметь точный тип.

@HerringtonDarkholme Спасибо. В моей первоначальной проблеме упоминалось об этом, но я пропустил это в конце, поскольку у кого-то в любом случае был бы лучший синтаксис, оказывается, они делают 😄

@DanielRosenwasser Это выглядит намного разумнее, спасибо!

@wallverb Я так не думаю, хотя я бы тоже хотел, чтобы эта функция существовала 😄

Что, если я хочу выразить объединение типов, где некоторые из них точны, а некоторые нет? Предлагаемый синтаксис сделает его подверженным ошибкам и трудным для чтения, даже если дополнительное внимание будет уделено интервалу:

|Type1| | |Type2| | Type3 | |Type4| | Type5 | |Type6|

Сможете ли вы быстро сказать, какие члены союза не точны?

А без аккуратного интервала?

|Type1|||Type2||Type3||Type4||Type5||Type6|

(ответ: Type3 , Type5 )

@rotemdan См. ответ выше, вместо него есть общий тип Extact который является более надежным предложением, чем мое. Я думаю, что это предпочтительный подход.

Также есть беспокойство по поводу того, как это будет выглядеть в подсказках редактора, всплывающих окнах предварительного просмотра и сообщениях компилятора. Псевдонимы типов в настоящее время просто «сглаживаются» до необработанных выражений типов. Псевдоним не сохраняется, поэтому непонятные выражения все равно будут отображаться в редакторе, если не будут приняты специальные меры для противодействия этому.

Мне трудно поверить, что этот синтаксис был принят в языке программирования, таком как Flow, у которого действительно есть объединения с тем же синтаксисом, что и Typescript. Мне кажется неразумным вводить ошибочный синтаксис, который принципиально противоречит существующему синтаксису, а затем очень стараться его «скрыть».

Одна интересная (забавная?) Альтернатива - использовать модификатор типа only . Думаю, у меня был черновик предложения по этому поводу несколько месяцев назад, но я так и не представил его:

function test(a: only string, b: only User) {};

Это был лучший синтаксис, который я мог найти на тот момент.

_Edit_: just тоже может работать?

function test(a: just string, b: just User) {};

_ (Edit: теперь, когда я вспоминаю, что синтаксис изначально был для модификатора для номинальных типов, но я думаю, это не имеет особого значения .. Эти две концепции достаточно близки, поэтому эти ключевые слова могут также работать здесь) _

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

  • just T (что означает: «точно T ») для точного структурного соответствия, как описано здесь.
  • only T (что означает: «однозначно T ») для номинального соответствия.

Номинальное соответствие можно рассматривать как еще более «строгий» вариант точного структурного соответствия. Это означало бы, что не только тип должен быть структурно идентичным, само значение должно быть связано с точно таким же идентификатором типа, как указано. Это может поддерживать или не поддерживать псевдонимы типов в дополнение к интерфейсам и классам.

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

_ (Edit: просто примечание о only при использовании с классами: здесь есть двусмысленность относительно того, допускает ли он номинальные подклассы при ссылке на базовый класс - я думаю, это нужно обсудить отдельно. в меньшей степени - то же самое можно было бы рассмотреть для интерфейсов - хотя в настоящее время я не думаю, что это было бы так полезно) _

Это похоже на замаскированные типы вычитания. Эти проблемы могут быть актуальными: https://github.com/Microsoft/TypeScript/issues/4183 https://github.com/Microsoft/TypeScript/issues/7993

@ethanresnick Почему ты так веришь?

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

(Возможно, другие ошибки, но не эта конкретная ошибка 😉)

Мне не нравится синтаксис канала, вдохновленный Flow. Что-то вроде ключевого слова exact за интерфейсами было бы легче читать.

exact interface Foo {}

@ mohsen1 Я уверен, что большинство людей будет использовать общий тип Exact в позициях выражений, так что это не должно иметь большого значения. Однако я был бы обеспокоен подобным предложением, поскольку вы могли бы преждевременно перегружать левую часть ключевого слова интерфейса, которое ранее было зарезервировано только для экспорта (в соответствии со значениями JavaScript - например, export const foo = {} ). Это также указывает на то, что, возможно, это ключевое слово также доступно для типов (например, exact type Foo = {} а теперь это будет export exact interface Foo {} ).

С {| |} синтаксисом как бы extends работа? будет ли interface Bar extends Foo {| |} точным, если Foo неточно?

Я думаю, что ключевое слово exact позволяет легко определить, точен ли интерфейс. Это может (должно?) Работать и для type .

interface Foo {}
type Bar = exact Foo

Чрезвычайно полезно для вещей, которые работают с базами данных или сетевыми вызовами баз данных или SDK, таких как AWS SDK, которые принимают объекты со всеми необязательными свойствами, поскольку дополнительные данные незаметно игнорируются и могут привести к тому, что трудно или очень трудно найти ошибки: rose:

@ mohsen1 Этот вопрос кажется несущественным для синтаксиса, поскольку тот же вопрос все еще существует с использованием подхода ключевых слов. Лично у меня нет предпочтительного ответа, и мне пришлось бы играть с существующими ожиданиями, чтобы ответить на него, но моя первоначальная реакция такова, что не имеет значения, является ли Foo точным или нет.

Использование ключевого слова exact кажется неоднозначным - вы говорите, что его можно использовать как exact interface Foo {} или type Foo = exact {} ? Что означает exact Foo | Bar ? Использование универсального подхода и работа с существующими шаблонами означает, что не требуется повторное изобретение или обучение. Это просто interface Foo {||} (это единственное новое здесь), затем type Foo = Exact<{}> и Exact<Foo> | Bar .

Мы говорили об этом довольно долго. Попробую подвести итог дискуссии.

Проверка лишнего имущества

Точные типы - это всего лишь способ обнаружить дополнительные свойства. Спрос на точные типы сильно упал, когда мы изначально внедрили проверку избыточных свойств (EPC). EPC был, вероятно, самым большим критическим изменением, которое мы сделали, но оно окупилось; почти сразу мы получили ошибки, когда EPC не обнаружил лишнее свойство.

По большей части, когда людям нужны точные типы, мы предпочли бы исправить это, сделав EPC умнее. Ключевой областью здесь является случай, когда целевой тип является типом объединения - мы хотим просто принять это как исправление ошибки (EPC должен работать здесь, но он еще не реализован).

Необязательные типы

С EPC связана проблема необязательных типов (которые я называю «слабыми»). Скорее всего, все слабые типы захотят быть точными. Нам просто нужно реализовать определение слабого типа (# 7485 / # 3842); Единственным препятствием здесь являются типы пересечений, реализация которых требует дополнительной сложности.

Чей тип точен?

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

На одном конце спектра у вас есть функции, которые буквально выбрасывают исключение (или иным образом делают плохие вещи), если объекту с собственным ключом предоставляется за пределами некоторого фиксированного домена. Таких немного и далеко друг от друга (не могу назвать пример по памяти). Посередине находятся функции, которые молча игнорируют
неизвестные свойства (почти все). А на другом конце у вас есть функции, которые обычно работают со всеми свойствами (например, Object.keys ).

Очевидно, что функции «будет выдавать, если будут предоставлены дополнительные данные» должны быть помечены как принимающие точные типы. А как насчет середины? Люди, скорее всего, не согласятся. Point2D / Point3D - хороший пример - можно разумно сказать, что функция magnitude должна иметь тип (p: exact Point2D) => number чтобы предотвратить передачу Point3D . Но почему я не могу передать объект { x: 3, y: 14, units: 'meters' } этой функции? Здесь на помощь приходит EPC - вы хотите обнаружить это «лишнее» свойство units в тех местах, где оно определенно отбрасывается, но не блокировать вызовы, связанные с псевдонимом.

Нарушение предположений / проблемы с инстанцией

У нас есть несколько основных принципов, которые не позволяют использовать точные типы. Например, предполагается, что тип T & U всегда присваивается T , но это не удается, если T является точным типом. Это проблематично, потому что у вас может быть какая-то универсальная функция, которая использует этот принцип T & U -> T , но вызывает функцию с T созданную с точным типом. Таким образом, у нас нет никакого способа сделать этот звук (на самом деле это не нормально для ошибки при создании экземпляра) - не обязательно блокировщик, но сбивает с толку то, что общая функция более разрешительна, чем созданная вручную версия самой себя!

Также предполагается, что T всегда присваивается T | U , но не очевидно, как применить это правило, если U - точный тип. Можно ли присвоить { s: "hello", n: 3 } { s: string } | Exact<{ n: number }> ? «Да» кажется неправильным ответом, потому что тот, кто ищет n и обнаруживает его, не будет рад увидеть s , но «Нет» также кажется неправильным, потому что мы нарушили базовый T -> T | U правило.

Разное

Что означает function f<T extends Exact<{ n: number }>(p: T) ? :смущенный:

Часто требуются точные типы, тогда как на самом деле вам нужно «автоматически разъединенное» объединение. Другими словами, у вас может быть API, который может принимать { type: "name", firstName: "bob", lastName: "bobson" } или { type: "age", years: 32 } но не хочет принимать { type: "age", years: 32, firstName: 'bob" } потому что произойдет что-то непредсказуемое. «Правильный» тип - это, возможно, { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } но, черт возьми, неприятно печатать. Мы могли бы потенциально подумать о сахаре для создания подобных типов.

Резюме: Необходимые варианты использования

Наш обнадеживающий диагноз заключается в том, что это, помимо относительно небольшого количества действительно закрытых API, является решением проблемы XY . По возможности мы должны использовать EPC для обнаружения «плохих» свойств. Поэтому, если у вас есть проблема, и вы думаете, что точные типы являются правильным решением, опишите исходную проблему здесь, чтобы мы могли составить каталог шаблонов и посмотреть, есть ли другие решения, которые были бы менее инвазивными / запутанными.

Я вижу, что в основном люди удивляются отсутствию точного типа объекта, так это поведение Object.keys и for..in - они всегда производят тип string вместо 'a'|'b' для чего-то набранного { a: any, b: any } .

Как я уже упоминал в https://github.com/Microsoft/TypeScript/issues/14094, и вы описали в разделе «Разное», раздражает, что {first: string, last: string, fullName: string} соответствует {first: string; last: string} | {fullName: string} .

Например, предполагается, что тип T & U всегда присваивается T, но это не удается, если T является точным типом.

Если T - точный тип, то предположительно T & U - это never (или T === U ). Верно?

Или U - неточное подмножество T

Мой вариант использования, который привел меня к этому предложению, - редукторы redux.

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

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

Правильно ли я понимаю, что присвоение значения T Exact<T> будет ошибкой?

interface Dog {
    name: string;
    isGoodBoy: boolean;
}
let a: Dog = { name: 'Waldo', isGoodBoy: true };
let b: Exact<Dog> = a;

В этом примере сужение Dog до Exact<Dog> небезопасно, верно?
Рассмотрим этот пример:

interface PossiblyFlyingDog extends Dog {
    canFly: boolean;
}
let c: PossiblyFlyingDog = { ...a, canFly: true };
let d: Dog = c; // this is okay
let e: Exact<Dog> = d; // but this is not

@leonadler Да, это была бы идея. Вы можете назначить Exact<T> только Exact<T> . Мой непосредственный вариант использования состоит в том, что функции проверки будут обрабатывать типы Exact (например, принимать полезные данные запроса как any и выводить действительный Exact<T> ). Exact<T> можно присвоить T .

@nerumo

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

Я наткнулся на ту же проблему и выяснил это решение, которое для меня является довольно элегантным обходным решением :)

export type State = {
  readonly counter: number,
  readonly baseCurrency: string,
};

// BAD
export function badReducer(state: State = initialState, action: Action): State {
  if (action.type === INCREASE_COUNTER) {
    return {
      ...state,
      counterTypoError: state.counter + 1, // OK
    }; // it's a bug! but the compiler will not find it 
  }
}

// GOOD
export function goodReducer(state: State = initialState, action: Action): State {
  let partialState: Partial<State> | undefined;

  if (action.type === INCREASE_COUNTER) {
    partialState = {
      counterTypoError: state.counter + 1, // Error: Object literal may only specify known properties, and 'counterTypoError' does not exist in type 'Partial<State>'. 
    }; // now it's showing a typo error correctly 
  }
  if (action.type === CHANGE_BASE_CURRENCY) {
    partialState = { // Error: Types of property 'baseCurrency' are incompatible. Type 'number' is not assignable to type 'string'.
      baseCurrency: 5,
    }; // type errors also works fine 
  }

  return partialState != null ? { ...state, ...partialState } : state;
}

вы можете найти больше в этом разделе моего руководства по сокращению:

Обратите внимание, что это можно решить в пользовательском пространстве, используя мое предложение типов ограничений (# 13257):

type Exact<T> = [
    case U in U extends T && T extends U: T,
];

Изменить: обновлен синтаксис относительно предложения

@piotrwitek спасибо, частичный трюк работает отлично и уже обнаружил ошибку в моей кодовой базе;) это стоит небольшого шаблонного кода. Но все же я согласен с @isiahmeadows, что было бы даже лучше

@piotrwitek с использованием Partial, подобного этому, почти решила мою проблему, но по-прежнему позволяет свойствам становиться неопределенными, даже если интерфейс State закрывает их (я предполагаю strictNullChecks).

Я закончил с чем-то более сложным, чтобы сохранить типы интерфейса:

export function updateWithPartial<S extends object>(current: S, update: Partial<S>): S {
    return Object.assign({}, current, update);
}

export function updateWith<S extends object, K extends keyof S>(current: S, update: {[key in K]: S[key]}): S {
    return Object.assign({}, current, update);
}

interface I {
    foo: string;
    bar: string;
}

const f: I = {foo: "a", bar: "b"}
updateWithPartial(f, {"foo": undefined}).foo.replace("a", "x"); // Compiles, but fails at runtime
updateWith(f, {foo: undefined}).foo.replace("a", "x"); // Does not compile
updateWith(f, {foo: "c"}).foo.replace("a", "x"); // Compiles and works

@asmundg, что правильно, решение примет undefined, но с моей точки зрения это приемлемо, потому что в своих решениях я использую только создателей действий с необходимыми параметрами для полезной нагрузки, и это гарантирует, что неопределенное значение никогда не будет присваивается свойству, не допускающему значения NULL.
Практически я довольно давно использую это решение в производстве, и этой проблемы никогда не возникало, но дайте мне знать, что вас беспокоит.

export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';

export const actionCreators = {
  changeBaseCurrency: (payload: string) => ({
    type: CHANGE_BASE_CURRENCY as typeof CHANGE_BASE_CURRENCY, payload,
  }),
}

store.dispatch(actionCreators.changeBaseCurrency()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.changeBaseCurrency(undefined)); // Argument of type 'undefined' is not assignable to parameter of type 'string'.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK => { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }

DEMO - включить strictNullChecks в опциях

вы также можете сделать полезную нагрузку с нулевым значением, если вам нужно, вы можете прочитать больше в моем руководстве: https://github.com/piotrwitek/react-redux-typescript-guide#actions

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

Предложение

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

Равенства до --strictTypes :

type A = { x: number, y: string };
type B = { x: number, y: string, ...restB: <T>T };
type C = { x: number, y: string, z: boolean, ...restC: <T>T };

declare const a: A;
declare const b: B;
declare const c: C;

a = b; // Error, type B has extra property: "restB"
a = c; // Error, type C has extra properties: "z", "restC"
b = a; // OK, restB inferred as {}
b = c; // OK, restB inferred as { z: boolean, ...restC: <T>T }

c = a; // Error, type A is missing property: "z"
       // restC inferred as {}

c = b; // Error, type B is missing property: "z"
       // restC inferred as restB 

Если --strictTypes не включен, свойство ...rest: <T>T автоматически добавляется к типу A . Таким образом, строки a = b; и a = c; больше не будут ошибками, как в случае с переменной b в двух следующих строках.

Несколько слов о нарушении предположений

предполагается, что тип T & U всегда присваивается T, но это не удается, если T является точным типом.

Да, & допускает ложную логику, но то же самое и с string & number . И string и number - разные жесткие типы, которые нельзя пересекать, однако система типов позволяет это. Точные типы также являются жесткими, поэтому несоответствие остается неизменным. Проблема кроется в операторе & - он ненадежен.

Можно ли присвоить {s: "hello", n: 3} {s: string} | Точное значение <{n: number}>.

Это можно перевести на:

type Test = { s: string, ...rest: <T>T } | { n: number }
const x: Test = { s: "hello", n: 3 }; // OK, s: string; rest inferred as { n: number }

Так что ответ должен быть «да». Точное объединение с неточными типами небезопасно, поскольку неточные типы включают все точные типы, если не присутствует свойство дискриминатора.

Re: функция f<T extends Exact<{ n: number }>(p: T) в комментарии @RyanCavanaugh выше, в одной из моих библиотек я бы очень хотел реализовать следующую функцию:

const checkType = <T>() => <U extends Exact<T>>(value: U) => value;

Т.е. функция, которая возвращает свой параметр с точно таким же типом, но в то же время также проверяет, является ли его тип точно таким же типом, что и другой (T).

Вот немного надуманный пример с тремя неудачными попытками удовлетворить оба требования:

  1. Никаких лишних свойств по отношению к CorrectObject
  2. Назначается HasX без указания HasX в качестве типа объекта
type AllowedFields = "x" | "y";
type CorrectObject = {[field in AllowedFields]?: number | string};
type HasX = { x: number };

function objectLiteralAssignment() {
  const o: CorrectObject = {
    x: 1,
    y: "y",
    // z: "z" // z is correctly prevented to be defined for o by Excess Properties rules
  };

  const oAsHasX: HasX = o; // error: Types of property 'x' are incompatible.
}

function objectMultipleAssignment() {
  const o = {
    x: 1,
    y: "y",
    z: "z",
  };
  const o2 = o as CorrectObject; // succeeds, but undesirable property z is allowed

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

function genericExtends() {
  const checkType = <T>() => <U extends T>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    z: "z", // undesirable property z is allowed
  }); // o is inferred to be { x: number; y: string; z: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

Здесь HasX - это сильно упрощенный тип (фактический тип сопоставляется с типом схемы), который определен на другом уровне, чем сама константа, поэтому я не могу создать тип o быть ( CorrectObject & HasX ).

С точными типами решение будет:

function exactTypes() {
  const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    // z: "z", // undesirable property z is *not* allowed
  }); // o is inferred to be { x: number; y: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

@ Энди-мс

Если T - точный тип, то предположительно T & U никогда не будет (или T === U). Верно?

Я думаю, что T & U должно быть never только если U доказуемо несовместимо с T , например, если T равно Exact<{x: number | string}> и U - это {[field: string]: number} , тогда T & U должно быть Exact<{x: number}>

См. Первый ответ на это:

Или U - неточное подмножество T

Я бы сказал, если U присваивается T, то T & U === T . Но если T и U - разные точные типы, тогда T & U === never .

В вашем примере, почему необходима функция checkType , которая ничего не делает? Почему бы просто не получить const o: Exact<CorrectObject> = { ... } ?

Потому что теряется информация о том, что x определенно существует (необязательно в CorrectObject) и является числом (число | строка в CorrectObject). Или, возможно, я неправильно понял, что означает Exact, я думал, что это просто предотвратит посторонние свойства, а не то, что это будет рекурсивно означать, что все типы должны быть точно такими же.

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

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

@ Энди-мс

Я бы сказал, если U присваивается T, тогда T & U === T. Но если T и U - разные точные типы, то T & U === никогда.

Оператор типа & является оператором пересечения, результатом которого является общее подмножество обеих сторон, которое не обязательно равно ни одному из них. Самый простой пример, который я могу придумать:

type T = Exact<{ x?: any, y: any }>;
type U = { x: any, y? any };

здесь T & U должно быть Exact<{ x: any, y: any }> , которое является подмножеством T и U , но ни один из T является подмножеством U (отсутствует x) или U является подмножеством T (отсутствует y).

Это должно работать независимо от того, являются ли T , U или T & U точными типами.

@magnushiie У вас есть хороший момент - точные типы могут ограничивать возможность назначения для типов с большей шириной, но все же допускать возможность назначения для типов с большей глубиной. Таким образом, вы можете пересечь Exact<{ x: number | string }> с Exact<{ x: string | boolean }> чтобы получить Exact<{ x: string }> . Одна из проблем заключается в том, что это на самом деле небезопасно, если x не только для чтения - мы могли бы исправить эту ошибку для точных типов, поскольку они означают выбор более строгого поведения.

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

interface T {
    [index: string]: string;
}

interface S {
    a: string;
    b: string;
}

interface P extends S {
    c: number;
}

declare function f(t: T);
declare function f2(): P;
const s: S = f2();

f(s); // Error because an interface can have more fields that is not conforming to an index signature
f({ a: '', b: '' }); // No error because literals is exact by default

Вот хитрый способ проверить точный тип:

// type we'll be asserting as exact:
interface TextOptions {
  alignment: string;
  color?: string;
  padding?: number;
}

// when used as a return type:
function getDefaultOptions(): ExactReturn<typeof returnValue, TextOptions> {
  const returnValue = { colour: 'blue', alignment: 'right', padding: 1 };
  //             ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.
  return returnValue
}

// when used as a type:
function example(a: TextOptions) {}
const someInput = {padding: 2, colour: '', alignment: 'right'}
example(someInput as Exact<typeof someInput, TextOptions>)
  //          ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.

К сожалению, в настоящее время невозможно сделать Exact assertion в качестве параметра типа, поэтому его нужно делать во время вызова (т.е. вам нужно помнить об этом).

Вот вспомогательные утилиты, необходимые для его работы (спасибо @ tycho01 за некоторые из них):

type Exact<A, B extends Difference<A, B>> = AssertPassThrough<Difference<A, B>, A, B>
type ExactReturn<A, B extends Difference<A, B>> = B & Exact<A, B>

type AssertPassThrough<Actual, Passthrough, Expected extends Actual> = Passthrough;
type Difference<A, Without> = {
  [P in DiffUnion<keyof A, keyof Without>]: A[P];
}
type DiffUnion<T extends string, U extends string> =
  ({[P in T]: P } &
  { [P in U]: never } &
  { [k: string]: never })[T];

См .: Детская площадка .

Хороший! Также могут быть интересны typelevel-ts ) и @pelotom ( type-zoo ). :)

Всем, кому интересно, я нашел простой способ принудительного применения точных типов в параметрах функции. Работает, по крайней мере, на TS 2.7.

function myFn<T extends {[K in keyof U]: any}, U extends DesiredType>(arg: T & U): void;

РЕДАКТИРОВАТЬ: я думаю, чтобы это сработало, вы должны указать литерал объекта непосредственно в аргументе; это не сработает, если вы объявите выше отдельную константу и передадите ее. : / Но один обходной путь - просто использовать распространение объекта на сайте вызова, то есть myFn({...arg}) .

РЕДАКТИРОВАТЬ: извините, я не читал, что вы упомянули только TS 2.7. Я там протестирую!

@vaskevich Я не могу заставить его работать, т.е. он не определяет colour как лишнее свойство :

Когда появляются условные типы (# 21316), вы можете сделать следующее, чтобы требовать точные типы в качестве параметров функции, даже для "несвежих" объектных литералов:

type Exactify<T, X extends T> = T & {
    [K in keyof X]: K extends keyof T ? X[K] : never
}

type Foo = {a?: string, b: number}

declare function requireExact<X extends Exactify<Foo, X>>(x: X): void;

const exact = {b: 1}; 
requireExact(exact); // okay

const inexact = {a: "hey", b: 3, c: 123}; 
requireExact(inexact);  // error
// Types of property 'c' are incompatible.
// Type 'number' is not assignable to type 'never'.

Конечно, если вы расширите тип, это не сработает, но я не думаю, что вы действительно можете что-то с этим поделать:

const inexact = {a: "hey", b: 3, c: 123} as Foo;
requireExact(inexact);  // okay

Мысли?

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

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

Еще один вариант использования. Я почти столкнулся с ошибкой из-за следующей ситуации, о которой не сообщается как об ошибке:

interface A {
    field: string;
}

interface B {
    field2: string;
    field3?: string;
}

type AorB = A | B;

const fixture: AorB[] = [
    {
        field: 'sfasdf',
        field3: 'asd' // ok?!
    },
];

( Детская площадка )

Очевидным решением для этого может быть:

type AorB = Exact<A> | Exact<B>;

Я видел обходной путь, предложенный в # 16679, но в моем случае это тип AorBorC (может расти), и каждый объект имеет несколько свойств, поэтому мне довольно сложно вручную вычислить набор свойств fieldX?:never для каждого типа.

@michalstocki Разве это не # 20863? Вы хотите, чтобы проверка избыточной собственности в профсоюзах была более строгой.

В любом случае, при отсутствии точных типов и строгой проверки избыточных свойств в объединениях, вы можете выполнять эти свойства fieldX?:never программно, а не вручную, используя условные типы :

type AllKeys<U> = U extends any ? keyof U : never
type ExclusifyUnion<U> = [U] extends [infer V] ?
 V extends any ? 
 (V & {[P in Exclude<AllKeys<U>, keyof V>]?: never}) 
 : never : never

А затем определите свой союз как

type AorB = ExclusifyUnion<A | B>;

который расширяется до

type AorB = (A & {
    field2?: undefined;
    field3?: undefined;
}) | (B & {
    field?: undefined;
})

автоматически. Он также работает для любых AorBorC .

Также см. Https://github.com/Microsoft/TypeScript/issues/14094#issuecomment -373780463 для исключительной реализации или реализации.

@jcalz Расширенный тип ExclusifyUnion не очень безопасен:

const { ...fields } = o as AorB;

fields.field3.toUpperCase(); // it shouldn't be passed

Поля fields являются обязательными.

Я не думаю, что это имеет какое-то отношение к точным типам, а к тому, что происходит, когда вы распространяете, а затем деструктурируете объект типа объединения. Любое объединение в конечном итоге сведется к единому типу, подобному пересечению, поскольку оно разбивает объект на отдельные свойства, а затем воссоединяет их; любые корреляции или ограничения между составляющими каждого союза будут потеряны. Не знаю, как этого избежать ... если это ошибка, это может быть отдельная проблема.

Очевидно, что все будет лучше, если вы сделаете защиту типов перед деструктуризацией:

declare function isA(x: any): x is A;
declare function isB(x: any): x is B;

declare const o: AorB;
if (isA(o)) {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
} else {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
  if (fields.field3) {
    fields.field3.toUpperCase(); // okay
  }
}

Не то чтобы это «решило» проблему, которую вы видите, но я ожидаю, что кто-то будет действовать с ограниченным союзом именно так.

Возможно, https://github.com/Microsoft/TypeScript/pull/24897 исправит проблему распространения

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

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
const sureIsTrue: (fact: true) => void = () => {};
const sureIsFalse: (fact: false) => void = () => {};



declare const x: string;
declare const y: number;
declare const xAndYAreOfTheSameType: AreSame<typeof x, typeof y>;
sureIsFalse(xAndYAreOfTheSameType);  // <-- no problem, as expected
sureIsTrue(xAndYAreOfTheSameType);  // <-- problem, as expected

Хотел бы я сделать это:

type Exact<A, B> = A extends B ? B extends A ? B : never : never;
declare function needExactA<X extends Exact<A, X>>(value: X): void;

Поможет ли функция, описанная в этой проблеме, в случае, когда пустой / индексированный интерфейс соответствует объектно-подобным типам, таким как функции или классы?

interface MyType
{
    [propName: string]: any;
}

function test(value: MyType) {}

test({});           // OK
test(1);            // Fails, OK!
test('');           // Fails, OK!
test(() => {});     // Does not fail, not OK!
test(console.log);  // Does not fail, not OK!
test(console);      // Does not fail, not OK!

Интерфейс MyType определяет только подпись индекса и используется как тип единственного параметра функции test . Параметр, переданный в функцию типа:

  • Литерал объекта {} , проходит. Ожидаемое поведение.
  • Числовая константа 1 не проходит. Ожидаемое поведение (_Аргумент типа '1' не может быть назначен параметру типа 'MyType' ._)
  • Строковый литерал '' не проходит. Ожидаемое поведение (_`Аргумент типа '""' не может быть назначен параметру типа 'MyType' ._)
  • Объявление стрелочной функции () => {} : Проходит. Не ожидаемое поведение. Возможно проходит, потому что функции являются объектами?
  • Метод класса console.log Проходит. Не ожидаемое поведение. Аналогично стрелочной функции.
  • Класс console проходит. Не ожидаемое поведение. Наверное, потому что классы - это объекты?

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

Извиняюсь, если это не по теме. Пока что эта проблема является наиболее близкой к проблеме, которую я объяснил выше.

@ Janne252 Это предложение могло бы вам косвенно помочь. Предполагая, что вы попробовали очевидный Exact<{[key: string]: any}> , вот почему это сработает:

  • Объектные литералы передаются должным образом, как и в случае с {[key: string]: any} .
  • Числовые константы не работают должным образом, поскольку литералы не могут быть присвоены {[key: string]: any} .
  • Строковые литералы не работают должным образом, поскольку они не могут быть присвоены {[key: string]: any} .
  • Функции и конструкторы классов терпят неудачу из-за их подписи call (это не строковое свойство).
  • Объект console проходит, потому что это просто объект (а не класс). JS не делает разделения между объектами и словарями «ключ-значение», и TS здесь ничем не отличается, кроме добавленной строковой полиморфной типизации. Кроме того, TS не поддерживает типы, зависящие от значений, а typeof - это просто сахар для добавления нескольких дополнительных параметров и / или псевдонимов типов - это не так волшебно, как кажется.

@blakeembrey @michalstocki @ Алексей-Быков
Это мой способ делать точные типы:

type Exact<A extends object> = A & {__kind: keyof A};

type Foo = Exact<{foo: number}>;
type FooGoo = Exact<{foo: number, goo: number}>;

const takeFoo = (foo: Foo): Foo => foo;

const foo = {foo: 1} as Foo;
const fooGoo = {foo: 1, goo: 2} as FooGoo;

takeFoo(foo)
takeFoo(fooGoo) // error "[ts]
//Argument of type 'Exact<{ foo: number; goo: number; }>' is not assignable to parameter of type 'Exact<{ //foo: number; }>'.
//  Type 'Exact<{ foo: number; goo: number; }>' is not assignable to type '{ __kind: "foo"; }'.
//    Types of property '__kind' are incompatible.
//      Type '"foo" | "goo"' is not assignable to type '"foo"'.
//        Type '"goo"' is not assignable to type '"foo"'."

const takeFooGoo = (fooGoo: FooGoo): FooGoo => fooGoo;

takeFooGoo(fooGoo);
takeFooGoo(foo); // error "[ts]
// Argument of type 'Exact<{ foo: number; }>' is not assignable to parameter of type 'Exact<{ foo: number; // goo: number; }>'.
//  Type 'Exact<{ foo: number; }>' is not assignable to type '{ foo: number; goo: number; }'.
//    Property 'goo' is missing in type 'Exact<{ foo: number; }>'.

Он работает с параметрами функций, возвратами и даже с заданиями.
const foo: Foo = fooGoo; // ошибка
Нет накладных расходов во время выполнения. Единственная проблема в том, что всякий раз, когда вы создаете новый точный объект, вы должны приводить его к его типу, но на самом деле это не имеет большого значения.

Я считаю, что исходный пример имеет правильное поведение: я ожидаю, что interface s будет открытым. Напротив, я ожидаю, что type s будут закрыты (а они закрываются только иногда ). Вот пример неожиданного поведения при написании типа MappedOmit :
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3

Тип MappedOmit в этом примере работает только для размеченных объединений. Для недискриминируемых объединений Typescript 3.2 проходит, когда передается любое пересечение типов в объединении.

Приведенные выше обходные пути с использованием as TypeX или as any для приведения имеют побочный эффект в виде сокрытия ошибок при строительстве !. Мы хотим, чтобы наш typechecker также помогал нам обнаруживать ошибки при построении! Кроме того, есть несколько вещей, которые мы можем генерировать статически из четко определенных типов. Обходные приемы, подобные описанным выше (или обходные пути номинального типа, описанные здесь: https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481), останавливают работу этих генераторов (хотя мы можем отфильтровать начальные поля _ , это болезненное соглашение этого абсолютно можно избежать).

@ aleksey-bykov fyi я думаю, что ваша реализация там на 99%, это сработало для меня:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

const value1 = {};
const value2 = {a:1};

// works
const exactValue1: Exact<{}, typeof value1> = value1;
const exactValue1WithTypeof: Exact<typeof value1, typeof value1> = value1;

// cannot assign {a:number} to never
const exactValue1Fail: Exact<{}, typeof value2> = value2;
const exactValue1FailWithTypeof: Exact<typeof value1, typeof value2> = value2;

// cannot assign {} to never
const exactValue2Fail: Exact<{a: number}, typeof value1> = value1;
const exactValue2FailWithTypeof: Exact<typeof value2, typeof value1> = value1;

// works
const exactValue2: Exact<{a: number}, typeof value2> = value2;
const exactValue2WithTypeof: Exact<typeof value2, typeof value2> = value2;

вау, пожалуйста, оставьте цветы здесь, подарки отправляются в эту корзину

Здесь можно сделать одно небольшое улучшение:
Используя следующее определение Exact эффективно создает вычитание B из A как A & never типов для всех B Уникальные ключи

type Omit<T, K> = Pick<T, Exclude<keyof T, keyof K>>;
type Exact<A, B = {}> = A & Record<keyof Omit<B, A>, never>;

Наконец, я хотел иметь возможность сделать это, не добавляя явное использование шаблона для второго аргумента B template. Я смог выполнить эту работу, обернув метод - не идеально, поскольку он влияет на время выполнения, но он полезен, если он вам действительно действительно нужен:

function makeExactVerifyFn<T>() {
  return <C>(x: C & Exact<T, C>): C => x;
}

Пример использования:

interface Task {
  title: string;
  due?: Date;
}

const isOnlyTask = makeExactVerifyFn<Task>();

const validTask_1 = isOnlyTask({
    title: 'Get milk',
    due: new Date()  
});

const validTask_2 = isOnlyTask({
    title: 'Get milk'
});

const invalidTask_1 = isOnlyTask({
    title: 5 // [ts] Type 'number' is not assignable to type 'string'.
});

const invalidTask_2 = isOnlyTask({
    title: 'Get milk',
    procrastinate: true // [ts] Type 'true' is not assignable to type 'never'.
});

@danielnmsft Кажется странным оставлять B в Exact<A, B> необязательным в вашем примере, особенно если это требуется для правильной проверки. В остальном, мне кажется, это неплохо. Хотя он выглядит лучше с именем Equal .

@drabinowitz Ваш тип Exact самом деле не соответствует тому, что здесь было предложено, и, вероятно, его следует переименовать в что-то вроде AreExact . Я имею в виду, вы не можете этого сделать со своим типом:

function takesExactFoo<T extends Exact<Foo>>(foo: T) {}

Однако ваш тип удобен для реализации точного типа параметра!

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

interface Foo {
    bar: any
}

function takesExactFoo <T>(foo: T & Exact<Foo, T>) {
                    //  ^ or `T extends Foo` to type-check `foo` inside the function
}

let foo = {bar: 123}
let foo2 = {bar: 123, baz: 123}

takesExactFoo(foo) // ok
takesExactFoo(foo2) // error

UPD1 Это не создаст +1 функцию времени выполнения, как в решении @danielnmsft, и, конечно, гораздо более гибко.

UPD2 Я только что понял, что Даниэль фактически сделал в основном тот же тип Exact что и @drabinowitz , но более компактный и, вероятно, лучший. Я также понял, что сделал то же самое, что и Даниэль. Но оставлю свой комментарий на тот случай, если кому-то это пригодится.

Это определение AreSame / Exact , похоже, не работает для типа объединения.
Пример: Exact<'a' | 'b', 'a' | 'b'> приводит к never .
Очевидно, это можно исправить, указав type AreSame<A, B> = A|B extends A&B ? true : false;

@nerumo определенно нашел это для той же функции редуктора, которую вы показали.

Пара дополнительных опций из того, что у вас было:

1 Вы можете установить тип возвращаемого значения таким же, как тип ввода, с помощью typeof . Более полезно, если это очень сложный тип. Для меня, когда я смотрю на это, становится более очевидным, что цель - предотвратить дополнительные свойства.

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): typeof state {
   return {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   };
}

2 Для редукторов вместо временной переменной присвойте ее самой себе перед возвратом:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {
   return (state = {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   });
}

3 Если вам действительно нужна временная переменная, не задавайте ей явного типа, снова используйте typeof state

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: typeof state = {
       ...state,
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

3b Если ваш редуктор не содержит ...state вы можете использовать Partial<typeof state> для типа:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: Partial<typeof state> = {
       name: 'Simon',
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

Я действительно чувствую, что весь этот разговор (и я просто прочитал всю ветку) упускает из виду суть проблемы для большинства людей, а именно то, что для предотвращения ошибок все, что нам нужно, - это утверждение типа, чтобы предотвратить запрет на более широкий тип:

Это то, что люди могут попробовать в первую очередь, что не запрещает fullName:

 return <State> {
       ...state,
       fullName: action.payload         // compiles ok :-(
   };

Это потому, что <Dog> cat вы говорите компилятору - да, я знаю, что делаю, это Dog ! Вы не спрашиваете разрешения.

Итак, что было бы наиболее полезно для меня, так это более строгая версия <Dog> cat , которая предотвратила бы посторонние свойства:

 return <strict State> {
       ...state,
       fullName: action.payload     // compiles ok :-(
   };

Вся эта штука типа Exact<T> имеет множество последствий (это длинный поток!). Это напоминает мне всю дискуссию о «проверенных исключениях», когда вы думаете, что хотите, но оказывается, что у нее много проблем (например, внезапно через пять минут вы хотите получить Unexact<T> ).

С другой стороны, <strict T> будет действовать скорее как барьер, предотвращающий «прохождение» «невозможных» типов. По сути, это фильтр типа, который проходит через тип (как это было сделано выше с функциями времени выполнения).

Однако новичкам было бы легко предположить, что это предотвращает прохождение «плохих данных» в тех случаях, когда это было бы невозможно.

Итак, если бы мне пришлось сделать синтаксис предложения, это было бы так:

/// This syntax is ONLY permitted directly in front of an object declaration:
return <strict State> { ...state, foooooooooooooo: 'who' };

Вернемся к OP: теоретически [1] с отрицательными типами можно написать type Exact<T> = T & not Record<not keyof T, any> . Тогда Exact<{x: string}> запретит присвоение ему любых типов с ключами, кроме x . Не уверен, что этого достаточно, чтобы удовлетворить все, о чем здесь спрашивают, но, похоже, это идеально подходит для OP.

[1] Я говорю теоретически, потому что это также основано на улучшении подписей индекса

Любопытно узнать, есть ли у меня проблема, описанная здесь. У меня есть такой код:

const Layers = {
  foo: 'foo'
  bar: 'bar'
  baz: 'baz'
}

type Groups = {
  [key in keyof Pick<Layers, 'foo' | 'bar'>]: number
}

const groups = {} as Groups

затем он позволяет мне устанавливать неизвестные свойства, чего я не хочу:

groups.foo = 1
groups.bar = 2
groups.anything = 2 // NO ERROR :(

Установка anything прежнему работает, а тип значения ключа - any . Я надеялся, что это будет ошибка.

Это то, что решит этот вопрос?

Оказывается, я должен был делать

type Groups = {
  [key in keyof Pick<typeof Layers, 'foo' | 'bar'>]: number
}

Обратите внимание на добавленное использование typeof .

Плагин Atom atom-typescript изо всех сил старался не потерпеть неудачу и в итоге вышел из строя. Когда я добавил typeof , все вернулось к норме, и неизвестные реквизиты больше не были разрешены, чего я ожидал.

Другими словами, когда я не использовал typeof , atom-typescript пытался определить тип в других местах кода, где я использовал объекты типа Groups , и это позволяло мне добавлять неизвестные реквизиты и показывать мне подсказку типа any для них.

Так что я не думаю, что у меня проблема с этой веткой.

Еще одна сложность может заключаться в том, как обрабатывать необязательные свойства.

Если у вас есть тип с дополнительными свойствами, что будет означать Exact<T> для этих свойств:

export type PlaceOrderResponse = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharged?: number
};

Означает ли Exact<T> все необязательные свойства? Как бы вы его обозначили? Не undefined или null, потому что это влияет на время выполнения.

Требуется ли теперь новый способ указать «обязательный необязательный параметр»?

Например, что нам нужно присвоить amountCharged в следующем примере кода, чтобы он удовлетворял «точности» типа? Мы не будем очень «точными», если не добьемся того, чтобы это свойство было хоть как-то «признано». Это <never> ? Это не может быть undefined или null .

const exactOrderResponse: Exact<PlaceOrderResponse> = 
{
   status: 'paymentFailed',
   orderNumber: '1001',
   amountCharged: ????      
};

Итак, вы можете подумать - это все еще необязательно, и теперь оно совершенно необязательно, что просто переводится как необязательное . И, конечно же, во время выполнения его не нужно было бы устанавливать, но мне кажется, что мы просто «сломали» Exact<T> , вставив вопросительный знак.

Может быть, эта проверка нужна только при присвоении значения между двумя типами? (Для обеспечения того, чтобы они оба включали amountCharged?: number )

Давайте представим здесь новый тип входных данных диалогового окна:

export type OrderDialogBoxData = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharge?: number      // note the typo here!
};

Итак, давайте попробуем это:

// run the API call and then assign it to a dialog box.
const serverResponse: Exact<PlaceOrderResponse> = await placeOrder();
const dialogBoxData: Exact<OrderDialogBoxData> = serverResponse;    // SHOULD FAIL

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

Тогда я вернулся к вопросу: «Почему мы вообще этого хотим?» .
Я думаю, что это было бы по следующим причинам (или подмножеству в зависимости от ситуации):

  • Избегайте опечаток в названиях свойств
  • Если мы добавляем свойство к какому-либо «компоненту», мы хотим убедиться, что все, что его использует, должно также добавить это свойство.
  • Если мы удаляем свойство из какого-то «компонента», нам нужно удалить его везде.
  • Убедитесь, что мы не предоставляем лишние свойства без надобности (возможно, мы отправляем их в API, и мы хотим сохранить полезную нагрузку).

Если «точные необязательные свойства» не обрабатываются должным образом, то некоторые из этих преимуществ не работают или сильно запутываются!

Также в приведенном выше примере мы просто «вставили» Exact чтобы избежать опечаток, но нам удалось только создать огромный беспорядок! И теперь он стал еще более хрупким, чем когда-либо прежде.

Я думаю, что мне часто нужен вовсе не тип Exact<T> , это один из этих двух:

NothingMoreThan<T> или
NothingLessThan<T>

Где «обязательный необязательный» теперь вещь. Первый не позволяет ничего лишнего определять в правой части присвоения, а второй гарантирует, что все (включая необязательные свойства) указано в правой части назначения.

NothingMoreThan было бы полезно для полезных нагрузок, отправляемых по сети, или JSON.stringify() и если вы получите сообщение об ошибке из-за того, что у вас слишком много свойств на RHS, вам придется написать код времени выполнения, чтобы выбрать только необходимые свойства. И это правильное решение, потому что именно так работает Javascript.

NothingLessThan - это то, что у нас уже есть в машинописном тексте - для всех обычных назначений - за исключением того, что необходимо учитывать необязательные свойства (optional?: number) .

Я не ожидаю, что эти имена вызовут какое-то внимание, но я думаю, что концепция более ясна и детализирована, чем Exact<T> ...

Тогда, возможно (если это действительно нужно):

Exact<T> = NothingMoreThan<NothingLessThan<T>>;

или было бы так:

Exact<T> = NothingLessThan<NothingMoreThan<T>>;   // !!

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

Заключительное примечание: NothingLessThan / NothingMoreThan имеют такое же «ощущение», что и некоторые из приведенных выше комментариев, где тип A расширен из типа B или B расширен из A. Ограничение заключается в том, что они не будут обращаться к необязательным свойствам (по крайней мере, я не думаю, что они могли бы сегодня).

@simeyla Вы могли бы просто обойтись без варианта "не более чем".

  • «Ни что иное, как» - это просто нормальные типы. TS делает это неявно, и каждый тип рассматривается как эквивалент for all T extends X: T .
  • «Ничего больше, чем», по сути, противоположное: это неявный for all T super X: T

Способа выбрать одно или оба явно было бы достаточно. В качестве побочного эффекта вы можете указать Java T super C качестве предлагаемого T extends NothingMoreThan<C> . Так что я почти уверен, что это, вероятно, лучше, чем стандартные точные типы.

Я считаю, что это должен быть синтаксис. Может быть, это?

  • extends T - Объединение всех типов, присваиваемых T, т.е. эквивалентно простому T .
  • super T - Объединение всех типов T присваивается.
  • extends super T , super extends T - Объединение всех типов, эквивалентных T. Это просто выпадает из сетки, поскольку только тип может быть как назначаемым, так и присвоенным самому себе.
  • type Exact<T> = extends super T - Sugar, встроенный в общий случай, описанный выше, для облегчения чтения.
  • Поскольку это просто переключает возможность присваивания, у вас все еще могут быть такие вещи, как союзы, которые являются точными или супертипами.

Это также позволяет реализовать # 14094 в пользовательском пространстве, просто создавая каждый вариант Exact<T> , например Exact<{a: number}> | Exact<{b: number}> .


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

Интересно, делает ли это также возможными отрицательные типы в пользовательском пространстве, поскольку (super T) | (расширяет T) эквивалентно unknown. Я считаю, что это так, но сначала мне нужно было бы проделать сложную арифметику типов, чтобы подтвердить это, и это не совсем очевидная вещь для доказательства.

Для (super T) | (extends T) === unknown чтобы удерживать присваиваемость, должен быть полным порядком.

@ jack-williams Хороший улов и исправил (сняв претензию). Мне было интересно, почему сначала что-то не получалось, когда я немного поигрался.

@ Джек-Уильямс

«Ни что иное, как» - это просто нормальные типы. TS делает это неявно, и каждый тип рассматривается как эквивалентный

Да и нет. Но в основном да ... ... но только если вы находитесь в режиме strict !

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

Что ж, это именно то, что вы получаете с lastName: string | undefined тогда как у меня в основном lastName?: string , и, конечно же, без режима strict вы не будете предупреждены обо всех несоответствиях.

Я всегда знал о строгом режиме, и я не могу найти вескую причину, по которой я не включил его до вчерашнего дня, но теперь, когда у меня есть (и я все еще пробираюсь через сотни исправлений ) гораздо проще добиться желаемого поведения «из коробки».

Я пробовал все, что хотел, включая игру с Required<A> extends Required<B> и попытки удалить необязательные флаги свойств ? . Это отправило меня в совершенно другую кроличью нору - (и все это было до того, как я включил режим strict ).

Дело в том , что если вы пытаетесь получить что - то близкое к «точным» типам сегодня , то вам нужно начать с включением strict режима (или любой комбинацией флагов дает правильные чеки). И если бы мне нужно было добавить middleName: string | undefined позже, то бум - я бы внезапно нашел все, что мне нужно было "рассмотреть" :-)

PS. спасибо за комментарии - было очень полезно. Я понимаю, что видел МНОГО кода, который явно не использует режим strict - а потом люди натыкаются на стены, как и я. Интересно, что можно сделать, чтобы еще больше стимулировать его использование?

@simeyla Я думаю, что ваши отзывы и благодарности должны быть адресованы @isiahmeadows!

Я решил, что опишу свой опыт работы с точными типами после реализации базового прототипа. Я считаю, что команда верна в своей оценке:

Наш обнадеживающий диагноз заключается в том, что это решение проблемы XY, помимо относительно небольшого числа действительно закрытых API.

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

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

Строгая типизация для keys и for ... in .

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

Укрепление необязательного расширения свойств.

Правило присваивания { ... } <: { ...; x?: T } является несостоятельным, потому что левый тип может включать несовместимое свойство x которому был назначен псевдоним. При назначении от точного типа это правило становится правильным. На практике я никогда не использую это правило; он кажется более подходящим для устаревших систем, в которых изначально не было бы точных типов.

Реагировать и HOC

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

Ограниченный универсальный объект позволяет указать нужные вам свойства и передать остальное. Как только граница становится точной, вы полностью теряете подтип ширины, и универсальный тип становится значительно менее полезным. Другая проблема заключается в том, что одним из основных инструментов композиции в TypeScript является пересечение, но типы пересечения несовместимы с точными типами. Любой нетривиальный тип пересечения с точным компонентом будет пустым: _точные типы не составляют_. Для response и props вам, вероятно, понадобятся типы строк и полиморфизм строк, но это на другой день.

Почти все интересные ошибки, которые могут быть решены с помощью точных типов, решаются избыточной проверкой свойств; Самая большая проблема заключается в том, что проверка избыточного свойства не работает для профсоюзов без дискриминантного свойства; решите это, и почти все интересные проблемы, относящиеся к конкретным типам, исчезнут, ИМО.

@ jack-williams Я согласен, что вообще не очень полезно иметь точные типы. Концепция проверки избыточных свойств фактически покрывается моим предложением оператора super T , просто косвенно, потому что объединение всех типов, которым присваивается T, в частности , не включает правильные подтипы T.

Я не особо поддерживаю это лично, за исключением, может быть, T super U *, поскольку почти единственный вариант использования, с которым я когда- либо сталкивался для проверки избыточных свойств, имел дело с неисправными серверами, что обычно можно обойти. использование функции-оболочки для генерации запросов вручную и удаления лишнего мусора. Все остальные проблемы, которые я обнаружил в этой ветке, могут быть решены просто с помощью простого размеченного объединения.

* В основном это будет T extends super U используя мое предложение - нижние границы иногда полезны для ограничения контравариантных универсальных типов, а обходные пути обычно приводят к появлению большого количества шаблонов дополнительных типов, по моему опыту.

@isiahmeadows Я, конечно, согласен с тем, что нижние ограниченные типы могут быть полезны, и если вы можете получить из этого точные типы, то это победа для тех, кто хочет их использовать. Думаю, мне следует добавить в свой пост предостережение: я в первую очередь обращаюсь к концепции добавления нового оператора специально для конкретных типов объектов.

@ jack-williams Я думаю, вы упустили мой нюанс, что я в первую очередь имел в виду точные типы и связанную с ними часть проверки лишних свойств. Немного о нижележащих ограниченных типах было сделано в сноске по какой-то причине - это было отступление, имеющее лишь косвенное отношение.

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

// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
   [P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}

// This can be used to implement partially strict typing e.g.:
// ('b?:' is where the behaviour differs with optional b)
type BaseOptions = { a: string, b: number }

// Checks there are no extra properties (Not More, Less fine)
const noMore = <T extends Subset<BaseOptions, T>>(options: T) => { }
noMore({ a: "hi", b: 4 })        //Fine
noMore({ a: 5, b: 4 })           //Error 
noMore({ a: "o", b: "hello" })   //Error
noMore({ a: "o" })               //Fine
noMore({ b: 4 })                 //Fine
noMore({ a: "o", b: 4, c: 5 })   //Error

// Checks there are not less properties (More fine, Not Less)
const noLess = <T extends Subset<T, BaseOptions>>(options: T) => { }
noLess({ a: "hi", b: 4 })        //Fine
noLess({ a: 5, b: 4 })           //Error
noLess({ a: "o", b: "hello" })   //Error
noLess({ a: "o" })               //Error  |b?: Fine
noLess({ b: 4 })                 //Error
noLess({ a: "o", b: 4, c: 5 })   //Fine

// We can use these together to get a fully strict type (Not More, Not Less)
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 })        //Fine
strict({ a: 5, b: 4 })           //Error
strict({ a: "o", b: "hello" })   //Error
strict({ a: "o" })               //Error  |b?: Fine
strict({ b: 4 })                 //Error
strict({ a: "o", b: 4, c: 5 })   //Error

// Or a fully permissive type (More Fine, Less Fine)
type Permissive<A extends {}, B extends {}> = Subset<A, B> | Subset<B, A>;
const permissive = <T extends Permissive<BaseOptions, T>>(options: T) => { }
permissive({ a: "hi", b: 4 })        //Fine
permissive({ a: 5, b: 4 })           //Error
permissive({ a: "o", b: "hello" })   //Error
permissive({ a: "o" })               //Fine
permissive({ b: 4 })                 //Fine
permissive({ a: "o", b: 4, c: 5 })   //Fine


Точный тип для присвоения переменной, который, как я понял, на самом деле ничего не делает ...

// This is a little unweildy, there's also a shortform that works in many cases:
type Exact<A extends {}> = Subset<A, A>
// The simpler Exact type works for variable typing
const options0: Exact<BaseOptions> = { a: "hi", b: 4 }        //Fine
const options1: Exact<BaseOptions> = { a: 5, b: 4 }           //Error
const options2: Exact<BaseOptions> = { a: "o", b: "hello" }   //Error
const options3: Exact<BaseOptions> = { a: "o" }               //Error |b?: Fine
const options4: Exact<BaseOptions> = { b: 4 }                 //Error
const options5: Exact<BaseOptions> = { a: "o", b: 4, c: 5 }   //Error

// It also works for function typing when using an inline value
const exact = (options: Exact<BaseOptions>) => { }
exact({ a: "hi", b: 4 })        //Fine
exact({ a: 5, b: 4 })           //Error
exact({ a: "o", b: "hello" })   //Error
exact({ a: "o" })               //Error  |b?: Fine
exact({ b: 4 })                 //Error
exact({ a: "o", b: 4, c: 5 })   //Error

// But not when using a variable as an argument even of the same type
const options6 = { a: "hi", b: 4 }
const options7 = { a: 5, b: 4 }
const options8 = { a: "o", b: "hello" }
const options9 = { a: "o" }
const options10 = { b: 4 }
const options11 = { a: "o", b: 4, c: 5 }
exact(options6)                 //Fine
exact(options7)                 //Error
exact(options8)                 //Error
exact(options9)                 //Error |b?: Fine
exact(options10)                //Error
exact(options11)                //Fine  -- Should not be Fine

// However using strict does work for that
// const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict(options6)                //Fine
strict(options7)                //Error
strict(options8)                //Error
strict(options9)                //Error |b?: Fine
strict(options10)               //Error
strict(options11)               //Error -- Is correctly Error

Видеть

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

Я чувствую, что у меня есть вариант использования для этого при упаковке компонентов React, где мне нужно «пропустить» реквизиты: https://github.com/Microsoft/TypeScript/issues/29883. @ jack-williams Есть мысли по этому поводу?

@OliverJAsh Выглядит актуально, но должен признать, что не так хорошо разбираюсь в React. Думаю, было бы полезно поработать, как именно здесь могут помочь точные типы.

type MyComponentProps = { foo: 1 };
declare const MyComponent: ComponentType<MyComponentProps>;

type MyWrapperComponent = MyComponentProps & { myWrapperProp: 1 };
const MyWrapperComponent: ComponentType<MyWrapperComponent> = props => (
    <MyComponent
        // We're passing too many props here, but no error!
        {...props}
    />
);

Пожалуйста, поправьте меня, если я скажу что-то не так.

Я предполагаю, что для начала нужно указать MyComponent чтобы принять точный тип?

declare const MyComponent: ComponentType<Exact<MyComponentProps>>;

В этом случае мы получим ошибку, но как исправить ошибку? Я предполагаю, что компоненты оболочки не имеют одного и того же типа опоры на всем ее протяжении, и в какой-то момент вам действительно нужно динамически извлекать подмножество опор. Это разумное предположение?

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

Если MyWrapperComponent не является точным, тогда потребуется некоторая проверка во время выполнения, чтобы доказать точность нового типа, что можно сделать только путем явного выбора нужных свойств (которые не масштабируются, как вы говорите в ваш ОП). Я не уверен, сколько вы выиграете в этом случае.

Вещи, которые я не рассмотрел, потому что не знаю, насколько они вероятны, являются общим случаем, где props - это некоторый общий тип, и где вам нужно комбинировать реквизиты, такие как { ...props1, ...props2 } . Это обычное дело?

@Kotarski Вы

У меня есть такой вариант использования:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

// I want this to error, because the 'c' should mean it prevents either AB or ABCD from being satisfied.
const foo: AB | ABCD = { a, b, c };

// I presume that I would need to do this:
const foo: Exact<AB> | Exact<ABCD> = { a, b, c };

@ ryami333 Для этого не нужны точные типы; это просто требует исправления избыточной проверки свойств: # 13813.

@ ryami333 Если вы хотите использовать дополнительный тип, у меня есть тип, который будет делать то, что вы хотите, а именно заставлять более строгую версию объединений:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD


type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

// Error now.
const foo: StrictUnion<AB | ABCD> = { a: "", b: "", c: "" };

@dragomirtitian Очаровательно. Мне любопытно почему

type KeyofV1<T extends object> = keyof T

дает другой результат, чем

type KeyofV2<T> = T extends object ? keyof T : never

Может ли кто-нибудь мне это объяснить?

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

KeyofV1< AB | ABCD > // 'a' | 'b'
KeyofV2< AB | ABCD > // 'a' | 'b' | 'c' | 'e'

V1 получает общие ключи объединения, V2 получает ключи каждого члена объединения и объединяет результат.

@weswigham Есть причина, по которой они должны возвращать разные результаты?

Да? Как я уже сказал, V1 получает _общие ключи_ для каждого члена союза, потому что аргумент keyof конечном итоге оказывается keyof (AB | ABCD) , что составляет всего лишь "A" | "B" , в то время как версия в условном выражении получает только один член объединения за раз благодаря условному распределению по входным данным, так что по сути это keyof AB | keyof ABCD .

@weswigham Значит, условное

type Union =
    (AB extends object ? keyof AB : never) |
    (ABCD extends object ? keyof ABCD : never)

Когда я читаю этот код, я обычно ожидаю, что проверка (AB | ABCD) extends object будет работать как единое целое, проверяя, что (AB | ABCD) присваивается object , а затем возвращает keyof (AB | ABCD) как единица, 'a' | 'b' . Неявное отображение кажется мне действительно странным.

@isiahmeadows Распределительные условные типы можно рассматривать как foreach для объединений. Они по очереди применяют условный тип к каждому члену объединения, и результатом является объединение каждого частичного результата.

Итак, UnionKeys<A | B> = UnionKeys<A> | UnionKeys<B> =(keyof A) | (keyof B)

Но только если условный тип распределяет, и он распределяет, только если тестируемый тип является параметром голого типа. Так:

type A<T> = T extends object ? keyof T : never // distributive
type B<T> = [T] extends [object] ? keyof T : never // non distributive the type parameter is not naked
type B<T> = object extends T ? keyof T : never // non distributive the type parameter is not the tested type

Спасибо, ребята, я думаю, что понял. Я перестроил это для своего понимания; Я считаю, что NegativeUncommonKeys полезен и сам по себе. Вот на случай, если это кому-то пригодится.

type UnionKeys<T> = T extends any ? keyof T : never;
type NegateUncommonKeys<T, TAll> = (
    Partial<
        Record<
            Exclude<
                UnionKeys<TAll>,
                keyof T
            >,
            never
        >
    >
) 

type StrictUnion<T, TAll = T> = T extends any 
  ? T & NegateUncommonKeys<T, TAll>
  : never;

Я также понимаю, почему там есть и T и TAll . «Эффект цикла», когда T проверяется и обнажается, означает, что применяется каждый элемент в объединении для T тогда как непроверенный TAll содержит исходное и полное объединение всех элементов.

@weswigham Да .. только мне кажется, что этот раздел читается так, как будто он был написан одним инженером компилятора для другого разработчика компилятора.

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

Что такое параметры голого типа? (а почему бы им не одеться 😄)

т.е. T относится к отдельным составляющим после того, как условный тип распределяется по типу объединения)

Буквально вчера у меня была дискуссия о том, что означает это конкретное предложение и почему был сделан акцент на слове «после».

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

Раздел справочника имеет для меня смысл и объясняет его гораздо лучше, но я все еще скептически отношусь к выбору дизайна. Для меня просто не имеет логического смысла, как такое поведение естественным образом следует из теоретико-множественной и теоретико-типовой точки зрения. Это просто выглядит слишком хакерским.

естественно вытекают из теории множеств и теории типов

Возьмите каждый элемент набора и разделите его в соответствии с предикатом.

Это распределительная операция!

Возьмите каждый элемент набора и разделите его в соответствии с предикатом.

Хотя это имеет смысл только тогда, когда вы говорите о наборах множеств (т. Е. О типе объединения), который начинает звучать намного больше как теория категорий.

@RyanCavanaugh Хорошо, позвольте мне уточнить: я интуитивно прочитал T extends U ? F<T> : G<T> как T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T) , при этом сравнение проводилось не по частям, а как законченный шаг. Это заметно отличается от «объединения для всех {if t ∈ U then F({t}) else G({t}) | t ∈ T} , которое в настоящее время является семантикой.

(Прошу прощения, если мой синтаксис немного отличается - мои знания теории типов полностью самостоятельны, поэтому я знаю, что не знаю всех синтаксических формализмов.)

Вопрос о том, какая операция более интуитивно понятна, вызывает бесконечные споры, но с текущими правилами легко сделать дистрибутивный тип недистрибутивным с помощью [T] extends [C] . Если бы по умолчанию был недистрибутивным, вам потребовалось бы какое-то новое заклинание на другом уровне, чтобы вызвать дистрибутивность. Это также отдельный вопрос, а какое поведение предпочтительнее; IME почти никогда не хочу не раздающий типа.

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

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

В нынешнем виде я остановлюсь и остановлюсь, прежде чем увести разговор слишком далеко от темы.

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

30572

Вот пример проблемы:

Я хочу указать, что конечная точка / служба API моих пользователей НЕ должна возвращать какие-либо дополнительные свойства (например, пароль), кроме тех, которые указаны в интерфейсе службы. Если я случайно возвращаю объект с дополнительными свойствами, мне нужна ошибка времени компиляции, независимо от того, был ли объект результата создан литералом объекта или иным образом.

Проверка каждого возвращенного объекта во время выполнения может быть дорогостоящей, особенно для массивов.

Излишняя проверка свойств в этом случае не помогает. Честно говоря, я думаю, что это шаткое решение с одним трюком и пони. Теоретически это должно было обеспечить вид опыта «это просто работает» - на практике это также источник путаницы. Вместо этого следовало бы реализовать точные типы объектов, они бы хорошо охватили оба варианта использования.

@babakness Ваш тип NoExcessiveProps подходит. Я думаю, они имеют в виду что-то вроде этого:

interface API {
    username: () => { username: string }
}

const api: API = {
    username: (): { username: string } => {
        return { username: 'foobar', password: 'secret'} // error, ok
    }
}

const api2: API = {
    username: (): { username: string } => {
        const id: <X>(x: X) => X = x => x;
        const value = id({ username: 'foobar', password: 'secret' });
        return value  // no error, bad?
    }
}

Как писатель типа API вы хотите, чтобы username просто возвращало имя пользователя, но любой разработчик может обойти это, потому что типы объектов не имеют ограничений по ширине. Это может быть применено только при инициализации литерала, что может делать или не делать разработчик. Тем не менее, я бы сильно отговорил кого-либо от попытки использовать точные типы для обеспечения безопасности на основе языка.

@spion

Излишняя проверка свойств в этом случае не помогает. Честно говоря, я думаю, что это шаткое решение с одним трюком и пони. Теоретически они должны были предоставить опыт "это просто работает".

EPC - это разумная и легкая конструкция, которая решает множество проблем. На самом деле точные типы не «просто работают». Для правильной реализации, поддерживающей расширяемость, требуется система совершенно другого типа.

@ jack-williams Конечно, были бы и другие способы проверки присутствия (проверки времени выполнения, где производительность не является проблемой, тесты и т.д.), но дополнительный во время компиляции неоценим для быстрой обратной связи.

Кроме того, я не имел в виду, что точные типы «просто работают». Я имел в виду, что EPC должен был «просто работать», но на практике он ограничен, запутан и небезопасен. В основном потому, что, если пытаться «сознательно» использовать его, вы обычно в конечном итоге простреливаете себе ногу.

edit: Да, я отредактировал, чтобы заменить «они» на «это», поскольку я понял, что это сбивает с толку.

@spion

Кроме того, я не имел в виду, что точные типы «просто работают». Я имел в виду, что EPC должен был «просто работать», но на практике он ограничен, запутан и небезопасен. В основном потому, что, если пытаться «сознательно» использовать его, вы обычно в конечном итоге простреливаете себе ногу.

Виноват. Прочтите исходный комментарий как

Теоретически они должны были предоставить опыт типа «это просто работает» [который был бы точными типами вместо EPC]

комментарий в [] является моим чтением.

Исправленное заявление:

Теоретически это должно было дать опыт "это просто работает".

намного яснее. Простите за неверное толкование!

type NoExcessiveProps<O> = {
  [K in keyof O]: K extends keyof O ? O[K] : never 
}

// no error
const getUser1 = (): {username: string} => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
} 

// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}>  = {username: 'a', password: 'b' }

// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
}


Результат для getUser2 удивителен, он кажется непоследовательным и должен приводить к ошибке времени компиляции. Какое понимание, почему это не так?

@babakness Ваш NoExcessiveProps просто возвращает T (ну, тип с теми же ключами, что и T ). В [K in keyof O]: K extends keyof O ? O[K] : never K всегда будет ключом O поскольку вы сопоставляете более keyof O . Ваш пример ошибки const потому что он запускает EPC так же, как если бы вы набрали его как {username: string} .

Если вы не против вызова дополнительной функции, мы можем зафиксировать фактический тип переданного объекта и выполнить пользовательскую форму проверки лишних свойств. (Я понимаю, что все дело в том, чтобы автоматически отлавливать этот тип ошибки, поэтому это может иметь ограниченное значение):

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked(foo) //ok
}

@dragomirtitian А ... да ... хороший момент! Итак, я пытаюсь понять вашу функцию checked . Я особенно озадачен

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    const bar = checked(foo) // error
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    const bar = checked(foo) // error!?
    return checked(foo) //ok
}

Назначение bar в getUser3 не выполняется. Ошибка выглядит как foo
image

Подробная информация об ошибке

image

Тип для bar здесь {} , что выглядит так, как будто это потому, что на checked

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

E нигде не назначается. Но если мы заменим typeof E на typeof {} , это не сработает.

Какой тип для E? Происходит ли какая-то контекстно-зависимая вещь?

@babakness Если нет другого места, из которого можно вывести параметр типа, машинописный текст выведет его из возвращаемого типа. Поэтому, когда мы присваиваем результат checked возврату getUser* , E будет возвращаемым типом функции, а T будет фактический тип значения, которое вы хотите вернуть. Если нет места для вывода E , по умолчанию будет просто {} и вы всегда будете получать ошибку.

Причина, по которой я сделал это так, заключалась в том, чтобы избежать каких-либо явных параметров типа, вы можете создать более явную его версию:

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked<{ username: string }>()(foo) //ok
}

Примечание. Подход с использованием каррированных функций необходим, поскольку у нас еще нет частичного вывода аргументов (https://github.com/Microsoft/TypeScript/pull/26349), поэтому мы не можем указать один параметр типа, а другие будут выведены в тот же звонок. Чтобы обойти это, мы указываем E в первом вызове и позволяем вывести T во втором вызове. Вы также можете кэшировать функцию cache для определенного типа и использовать кешированную версию

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}
const checkUser = checked<{ username: string }>()

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checkUser(foo) //ok
}

FWIW это правило tslint WIP / sketch, которое решает конкретную проблему, заключающуюся в том, чтобы случайно не вернуть дополнительные свойства из "открытых" методов.

https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32

Для случая использования распространения - см. Https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -300382189 - может ли линтер обнаружить подобный шаблон и предупредить, что он небезопасен по типу?

Копирование примера кода из вышеупомянутого комментария:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

cc @JamesHenry / @ armano2

Очень бы хотелось, чтобы это произошло. Мы используем сгенерированные определения TypeScript для конечных точек GraphQL, и проблема в том, что TypeScript не вызывает ошибку, когда я передаю объект с большим количеством полей, чем необходимо для запроса, потому что GraphQL не сможет выполнить такой запрос во время выполнения.

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

если у вас есть проблема, и вы думаете, что точные типы - правильное решение, опишите исходную проблему здесь

https://github.com/microsoft/TypeScript/issues/12936#issuecomment -284590083

Вот один с участием React refs: https://github.com/microsoft/TypeScript/issues/31798

/ cc @RyanCavanaugh

Один из вариантов использования для меня -

export const mapValues =
  <T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
    type TResult = Exact<{ [K in keyof T]: V }>;
    const result: Partial<TResult> = { };
    for (const [key, value] of Object.entries(object)) {
      result[key] = mapper(value, key);
    }
    return result as TResult;
  };

Это неразумно, если мы не используем точные типы, поскольку, если object имеет дополнительные свойства, небезопасно вызывать mapper для этих дополнительных ключей и значений.

Настоящая мотивация здесь в том, что я хочу иметь где-нибудь значения для перечисления, которые я могу повторно использовать в коде:

const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));

где this.props.callback имеет тип (keyof typeof choices) => void .

Так что на самом деле речь идет о том, что система типов может представить тот факт, что у меня есть список ключей в области кода, который точно соответствует набору (например, объединению) ключей в области типов, так что мы можем писать функции, которые работают с этим список ключей и сделайте допустимые утверждения типа о результате. Мы не можем использовать объект ( choices в моем предыдущем примере), потому что, насколько известно системе типов, объект кодовой области может иметь дополнительные свойства помимо любого используемого типа объекта. Мы не можем использовать массив ( ['choice0', 'choice1', 'choice2'] as const , потому что, насколько известно системе типов, массив может не содержать всех ключей, разрешенных типом массива.

Может быть, exact не должен быть типом, а только модификатором входных и / или выходных данных функции? Что-то вроде модификатора дисперсии потока ( + / - )

Я хочу добавить к тому, что только что сказал @phaux . На самом деле я использую Exact для того, чтобы компилятор гарантировал форму функций. Когда у меня есть фреймворк, мне может понадобиться одно из них: (T, S): AtMost<T> , (T, S): AtLeast<T> или (T, S): Exact<T> где компилятор может проверить, что функции, определяемые пользователем, точно подходят.

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

Поможет ли это предотвратить это?

interface IDate {
  year: number;
  month: number;
  day: number;
}

type TBasicField = string | number | boolean | IDate;

 // how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
  // ... do things with basic field of only the exactly correct structures
}

const notADate = {
  year: 2019,
  month: 8,
  day: 30,
  name: "James",
};

doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking

Нам действительно нужен способ в TS сказать T extends exactly { something: boolean; } ? xxx : yyy .

Или иначе, что-то вроде:

const notExact = {
  something: true,
  name: "fred",
};

Все равно вернет там xxx .

Может быть, можно использовать ключевое слово const ? например, T extends const { something: boolean }

@pleerock это может быть немного двусмысленным, поскольку в JavaScript / TypeScript мы можем определить переменную как const но все же добавлять / удалять свойства объекта. Я думаю, что ключевое слово exact вполне уместно.

Я не уверен, что это точно связано, но в этом случае я ожидал бы как минимум две ошибки:
детская площадка
Screen Shot 2019-08-08 at 10 15 34

@mityok Я думаю, это связано. Я предполагаю, что вы хотели бы сделать что-то вроде:

class Animal {
  makeSound(): exact Foo {
     return { a: 5 };
  }
}

Если exact сделал тип более строгим - тогда его нельзя расширять дополнительным свойством, как вы сделали в Dog .

используя преимущества const ( as const ) и используя до интерфейсов и типов, таких как

const type WillAcceptThisOnly = number

function f(accept: WillAcceptThisOnly) {
}

f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast

const n: WillAcceptThisOnly = 1
f(n) // ok

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

Я придумал решение на чистом TypeScript для проблемы Exact<T> которое, как мне кажется, ведет себя точно так же, как было запрошено в основном сообщении:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

Причина, по которой ExactInner должно быть включено в Exact , связана с тем, что исправление # 32824 еще не выпущено (но уже объединено с ! 32924 ).

Присвоить значение переменной или аргументу функции типа Exact<T> случае, если правое выражение также равно Exact<T> , где T - это точно идентичный тип в обеих частях. присвоения.

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

Он работает, используя тот факт, что TypeScript использует проверку отношения extend чтобы определить, может ли правый тип быть назначен левому типу - это возможно только в том случае, если правый тип (источник) _ расширяет_ левый тип (назначение) .

Цитируя checker.ts ,

// Два условных типа 'T1 расширяет U1? X1: Y1 'и' T2 расширяет U2? X2: Y2 'связаны, если
// один из T1 и T2 связан с другим, U1 и U2 являются идентичными типами, X1 относится к X2,
// а Y1 относится к Y2.

ExactInner<T> generic использует описанный подход, заменяя U1 и U2 базовыми типами, требующими проверки точности. Exact<T> добавляет пересечение с простым базовым типом, что позволяет TypeScript ослаблять точный тип, когда его целевая переменная или аргумент функции не являются точным типом.

С точки зрения программиста, Exact<T> ведет себя так, как если бы он устанавливает флаг exact для T , без проверки T или его изменения и без создания независимого типа.

Вот ссылка на игровую площадку и ссылка на суть .

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

Потрясающая работа @toriningen!

Если кто-то сможет найти способ сделать эту работу без необходимости обертывать ваше значение в вызове exact это было бы идеально.

Не уверен, что это правильный вопрос, но вот пример того, над чем я бы хотел работать.

https://www.typescriptlang.org/play/#code/KYOwrgtgBAyg9gJwC4BECWDgGMlriKAbwCgooBBAZyygF4oByAQ2oYBpSoVhq7GATHlgbEAvsWIAzMCBx4CTfvwDyCQQgBCATwAU -DNlz4AXFABE5GAGEzUAD7mUAUWtmAlEQnjiilWuCauvDI6Jhy + AB0VFgRSHAAqgAOiQFWLMA6bm4A3EA

enum SortDirection {
  Asc = 'asc',
  Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());

@lookfirst Это другое. Это запрашивает функцию для типов, которые не допускают дополнительных свойств, например некоторого типа exact {foo: number} где {foo: 1, bar: 2} ему не присваивается. Это просто запрос на применение текстовых преобразований к значениям перечисления, которых, скорее всего, не существует.

Не уверен, что это правильный вопрос, но [...]

По моему опыту в качестве сопровождающего в другом месте, если вы сомневаетесь и не можете найти какой-либо явной существующей проблемы, сообщите о новой ошибке и наихудшем сценарии, она закрывается как обман, которого вы не нашли. Это в значительной степени относится к большинству крупных проектов JS с открытым исходным кодом. (Большинство из нас, крупных сопровождающих в сообществе JS, на самом деле порядочные люди, просто люди, которые могут сильно увязнуть в отчетах об ошибках и тому подобном, поэтому иногда трудно не быть действительно кратким.)

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

Другой пример использования:

https://www.typescriptlang.org/play/index.html#code/JYOwLgpgTgZghgYwgAgIoFdoE8AKcpwC2EkUAzsgN4BQydycAXMmWFKAOYDct9ARs1bsQ3agF9q1GOhAIwwAPYhkCKBDiQAKgAtgUACZ4oYXHA4QAqgCUAMgAoAjpiimChMswzYjREtDIAlFS8dGpg6FDKAAbaYGAADh4A9EkQAB5E8QA2EAB0CAqESQD8ACSUIBAA7sjWNgDK6lAI2j7udgDyfABWEHK5EODsEGSOzq5EgQFiUeKSKcgAolBQCuQANAwU6fF9kPrUqupaugZGJnjmdXaUDMwA5PebAsiPmwgPYLpkALTx + EQflVgFksj8EHB0GQID8vnp9H98CZEeZYQpwQQyNp7sgxAEeIclKxkP83BQALxUO6vJ7IF5vFSfb6ItxAkFgiFQmFwgws5H-VFgdGqOBYnFiAkLQC8G4BGPeQJl2Km0fQA1pxkPoIDBjhB9HSsFsyMAOCB1cAwPKFAwSQDiKRDmoNBAdPDzqYrrY7KTJvigA

EDITED: проверьте решение @aigoncharov ниже , потому что я думаю, что оно еще быстрее.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

Не знаю, можно ли это еще улучшить.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Без комментариев

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

Не знаю, можно ли это еще улучшить.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Без комментариев

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

Обожаю эту идею!

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

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: string
}
type B = {
  prop1: string
  prop2: string
}
type C = {
  prop1: string
}

type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>

http://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAEEiUAN58w0gPZgAjKLrBpASyoBzRgF9l4aACFNOlnsMmoZyzd0GYABMpuZWtg4q0ADCbgFeoX4RjI6qAMoAFvoArgA2NM4QAHKSMprwyGhq2M54qdCZOfmFGsQVKKjVUNF1QkA

Еще одна площадка с @iamandrewluca https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA

Здесь есть нюанс: можно ли назначить Exact<{ prop1: 'a' }> Exact<{ prop1: string }> . В моих случаях это должно быть.

@jeremybparagon ваше дело покрыто. Вот еще несколько случаев.

type InexactType = {
    foo: 'foo'
}

const obj = {
    // here foo is infered as `string`
    // and will error because `string` is not assignable to `"foo"`
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
    foo: 'foo'
}

const obj = {
    // here we cast to `"foo"` type
    // and will not error
    foo: 'foo' as 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

Я думаю, что любой, кто использует этот трюк (и я не говорю, что он не подходит для использования), должен четко осознавать, что очень легко получить больше реквизита в «точном» типе. Поскольку InexactType присваивается Exact<T, InexactType> если у вас есть что-то вроде этого, вы нарушаете точность, даже не осознавая этого:

function test1<T>(t: Exact<T, InexactType>) {}

function test2(t: InexactType) {
  test1(t); // inexactType assigned to exact type
}
test2(obj) // but 

Ссылка на игровую площадку

Это причина (по крайней мере, одна из них) того, что TS не имеет точных типов, поскольку для этого потребуется полное разветвление типов объектов в точных и неточных типах, где неточный тип никогда не может быть назначен точному, даже если по номинальной стоимости они совместимы. Неточный тип всегда может содержать больше свойств. (По крайней мере, это одна из причин, по которой @ahejlsberg упоминается как tsconf).

Если бы asExact был синтаксическим способом обозначения такого точного объекта, такое решение могло бы выглядеть так:

declare const exactMarker: unique symbol 
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

type InexactType = {
    foo: string
}
function asExact<T>(o: T): T & IsExact { 
  return o as T & IsExact;
}

const obj = asExact({
  foo: 'foo',
});


function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {

}

function test2(t: InexactType) {
  test1(t); // error now
}
test2(obj) 
test1(obj);  // ok 

const obj2 = asExact({
  foo: 'foo',
  bar: ""
});
test1(obj2);

const objOpt = asExact < { foo: string, bar?: string }>({
  foo: 'foo',
  bar: ""
});
test1(objOpt);

Ссылка на игровую площадку

@dragomirtitian , поэтому я придумал решение немного раньше https://github.com/microsoft/TypeScript/issues/12936#issuecomment -524631270, которое от этого не страдает.

@dragomirtitian - это вопрос того, как вы вводите свои функции.
Если вы сделаете это немного по-другому, это сработает.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}

function test2<T extends InexactType>(t: T) {
  test1(t); // fails
}
test2(obj)

https://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEogl0YsnOA8qgKIKwAmDE5KWgYNckbdcsqSNuD + 4oqWgG4oAHoNqGMEGwAbOnTC4EGy3z82oA

@jeremybparagon ваше дело покрыто.

@iamandrewluca Я думаю, что решения здесь и здесь различаются в зависимости от того, как они относятся к моему примеру .

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: 'a'
}
type C = {
  prop1: string
}

type ShouldBeA = Exact<A, C> // This evaluates to never.

const ob...

Ссылка на игровую площадку

@aigoncharov Проблема в том, что вам нужно знать об этом, чтобы этого легко не сделать, а test1 все равно можно было вызвать с дополнительными свойствами. ИМО, любое решение, которое может так легко допускать случайное неточное присвоение, уже потерпело неудачу, поскольку весь смысл заключается в обеспечении точности в системе типов.

@toriningen да, ваше решение кажется лучше, я просто имел в виду последнее опубликованное решение. В вашем решении есть тот факт, что вам не нужен дополнительный параметр типа функции, однако он, похоже, не работает для дополнительных свойств:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;

const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ? 

Ссылка на игровую площадку

@jeremybparagon Я не уверен, что решение @aigoncharov хорошо справляется с необязательными свойствами. Любое решение, основанное на T extends S и S extends T будет страдать от того простого факта, что

type A = { prop1: string }
type C = { prop1: string,  prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y 
type AextendsC = A extends C ? "Y" : "N" // also Y 

Ссылка на игровую площадку

Я думаю, что @iamandrewluca использования Exclude<keyof T, keyof Shape> extends never - это хорошо, мой тип очень похож (я отредактировал свой исходный ответ, добавив &R чтобы гарантировать T extends R без каких-либо дополнительных проверок).

type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

Я бы не стал ставить свою репутацию на карту тем, что в моем решении нет дыр, я не так усердно их искал, но приветствую любые такие открытия 😊

у нас должен быть флаг, где это включено глобально. Таким образом, тот, кто хочет потерять типаж, может продолжать делать то же самое. Слишком много ошибок, вызванных этой проблемой. Теперь я стараюсь избегать оператора спреда и использую pickKeysFromObject(shipDataRequest, ['a', 'b','c'])

Вот пример использования точных типов, на которые я недавно наткнулся:

type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;

const getFriendlyNameForKey = (key: PossibleKeys) => {
    switch (key) {
        case 'x':
            return 'Ecks';
        case 'y':
            return 'Why';
        case 'z':
            return 'Zee';
    }
};

const myMap: ImmutableMap = { x: 'foo', y: 'bar' };

const renderMap = (map: ImmutableMap) =>
    Object.keys(map).map(key => {
        // Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
        const friendlyName = getFriendlyNameForKey(key);
        // No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.    
        return [friendlyName, map[key]];
    });
;

Поскольку по умолчанию типы неточны, Object.keys должен возвращать string[] (см. Https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), но в этом случае , если ImmutableMap было точным, нет причин, по которым он не может вернуть PossibleKeys[] .

@dallonf обратите внимание, что этот пример требует дополнительных функций помимо точных типов - Object.keys - это просто функция, и должен быть какой-то механизм для описания функции, которая возвращает keyof T для точных типов и string для других типов. Просто иметь возможность объявить точный тип было бы недостаточно.

@RyanCavanaugh Я думаю, это было следствием, точные типы + способность их обнаруживать.

Вариант использования для реагирующих типов:

forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P> .

Заманчиво передать обычный компонент в forwardRef поэтому React выдает предупреждения во время выполнения, если обнаруживает propTypes или defaultProps в аргументе render . Мы хотели бы выразить это на уровне типа, но нам нужно вернуться к never :

- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>

Сообщение об ошибке с never бесполезно ("{} не может быть присвоено неопределенному").

Может ли кто-нибудь помочь мне узнать, как будет выглядеть решение @toriningen с объединением различных форм объектов события? Я хочу ограничить свои формы событий в вызовах redux-dispatch, например:

type StoreEvent =
  | { type: 'STORE_LOADING' }
  | { type: 'STORE_LOADED'; data: unknown[] }

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

(ОБНОВЛЕНИЕ: я понял: https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)

Пример использования:

Отсутствие поддержки Exact<> приводит к проблемам выполнения с мутациями GraphQL. GraphQL принимает точный список разрешенных свойств. Если вы предоставите слишком много реквизита, это вызовет ошибку.

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

Следующий пример иллюстрирует воображаемую безопасность.

  • в первом случае отслеживаются все входные параметры
  • но в реальной жизни (как во втором случае, когда мы получили данные из формы и сохранили их в какую-то переменную)

Попробуйте на детской площадке

Screen Shot 2020-03-05 at 13 04 38

Согласно статье https://fettblog.eu/typescript-match-the-exact-object-shape/ и аналогичным решениям, приведенным выше, мы можем использовать следующее уродливое решение:

Screen Shot 2020-03-05 at 12 26 57

Почему это решение savePerson<T>(person: ValidateShape<T, Person>) уродливо?

Предположим, у вас есть глубоко вложенный тип ввода, например:

// Assume we are in the ideal world where implemented Exact<>

type Person {
  name: string;
  address: Exact<Address>;
}

type Address {
   city: string
   location: Exact<Location>
}

type Location {
   lon: number;
   lat: number; 
}

savePerson(person: Exact<Person>)

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

savePerson<T, TT, TTT>(person: 
  ValidateShape<T, Person keyof ...🤯...
     ValidateShape<TT, Address keyof ...💩... 
         ValidateShape<TTT, Location keyof ...🤬... 
> > >)

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

Случай, описанный на первом изображении, когда TS не проверяет лишние свойства из-за потери «свежести», также стал для нас немного болезненной точкой.

Пишу

doSomething({
  /* large object of options */
})

часто кажется гораздо менее читаемым, чем

const options = {
  /* large object of options */
}
doSomething(options)

Явное аннотирование const options: DoSomethingOptions = { помогает, но это немного громоздко, и его трудно обнаружить и применить при проверке кода.

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

@RyanCavanaugh, спасибо за объяснение EPC ... Обсуждается ли где-нибудь более подробно разница между EPC и точными типами? Теперь я чувствую, что должен лучше понять, почему EPC допускает некоторые случаи, которые не допускаются конкретными типами.

Привет, @noppa, я думаю, это была бы отличная идея. Я только что наткнулся на это, когда заметил разницу между прямым назначением и первым назначением переменной - даже задал вопрос о SO, который привел меня сюда. Нынешнее поведение удивительно, по крайней мере, для меня ...

Я считаю, что у меня та же проблема, что и в примере с мутациями GraphQL (точная вложенная типизация, никаких дополнительных свойств не допускается). В моем случае я думаю о том, чтобы набрать ответы API в общем модуле (разделяемом между интерфейсом и сервером):

export type ProductsSlashResponse = {
  products: Array<{
    id: number;
    description: string;
  }>,
  total: number;
};

На стороне сервера я хотел бы убедиться, что ответ учитывает эту сигнатуру типа:

router.get("products/", async () =>
  assertType<ProductsSlashResponse>(getProducts())));

Я пробовал решения здесь. Кажется, что работает T extends U ? U extends T ? T : never : never вместе с каррированной функцией, которая не идеальна. Основная проблема заключается в том, что вы не получаете обратной связи об отсутствующих или дополнительных свойствах (возможно, мы могли бы улучшить это, но это становится трудно сделать, когда мы переходим к вложенным свойствам). Другие решения не работают с глубоко вложенными объектами.

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

@ fer22f GraphQL не отправляет поля, которые клиент не запрашивал ... если вы не используете скалярный тип JSON для products или для элементов массива, здесь не о чем беспокоиться.

Извините, я неправильно прочитал, я думал, вы имели в виду, что используете GraphQL

Кто-то уже упоминал GraphQL, но только с точки зрения "сбора вариантов использования" ( @DanielRosenwasser упоминал несколько лет назад в ветке :-) о "отсутствии каких-либо вариантов использования вне руки"), два варианта использования, в которых я хотел используйте Exact :

  1. Передача данных в хранилища данных / базы данных / ORM - любые переданные дополнительные поля будут автоматически отброшены / не сохранены.

  2. Передача данных в проводные вызовы / RPC / REST / GraphQL - снова любые переданные лишние поля будут отброшены / не отправлены.

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

В обоих случаях я хотел бы сказать программисту / себе (через ошибку компиляции): «... вам действительно не следует давать мне это дополнительное свойство, b / c, если вы ожидаете, что оно будет 'сохранено' или ' отправлено, не будет ».

Это особенно необходимо в API в стиле «частичное обновление», то есть в слабых типах:

type Data = { firstName:? string; lastName?: string; children?: [{ ... }] };
const data = { firstName: "a", lastNmeTypo: "b" };
await saveDataToDbOrWireCall(data);

Проходит проверку слабого типа b / c, по крайней мере, один параметр соответствует, firstName , поэтому он не на 100% непересекающийся, однако все еще есть «очевидная» опечатка lsatNmeTypo которая не улавливается.

Конечно, EPC работает, если я:

await saveDataToDbOrWireCall({ firstName, lastNmeTypo });

Но деструктуризация + перепечатка каждого поля довольно утомительна.

Такие решения, как Exactify @jcalz, работают со свойством 1-го уровня, но рекурсивный случай (т.е. children - это массив, и элементы массива должны быть точными), с которыми я борюсь, когда он попадает варианты использования в «реальном мире» с обобщениями / как Exact<Foo<Bar<T>> .

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

(В FWIW https://github.com/stephenh/joist-ts/pull/35/files есть моя текущая попытка глубокого Exact а также Exact.test.ts который проходит тривиальные случаи, но сам PR имеет ошибки компиляции при более эзотерическом использовании. Отказ от ответственности Я действительно не ожидаю, что кто-то будет изучать этот конкретный PR, но я просто предоставляю это как «вот где Exact было бы полезно» + «AFAICT это трудно сделать в "точке данных" на уровне пользователя.)

Привет,

Интересно, что думает команда TS относительно предложений о точных типах и кортежах? https://github.com/tc39/proposal-record-tuple

Имеет ли смысл вводить точные типы для этих новых примитивов?

@slorber Не TS, но это ортогонально. Это предложение касается неизменности, и проблемы почти идентичны между этим и такими библиотеками, как Immutable.js.

Я повторил рекурсивную версию @stephenh . У меня возникли проблемы с правильной обработкой неопределенных случаев с помощью рекурсии, я открыт для более чистого решения. Вероятно, это не работает в некоторых крайних случаях с массивами или сложной структурой данных.

export type Exact<Expected, Actual> = Expected &
  Actual & // Needed to infer `Actual`
  (null extends Actual
    ? null extends Expected
      ? Actual extends null // If only null stop here, because NonNullable<null> = never
        ? null
        : CheckUndefined<Expected, Actual>
      : never // Actual can be null but not Expected: forbid the field
    : CheckUndefined<Expected, Actual>);

type CheckUndefined<Expected, Actual> = undefined extends Actual
  ? undefined extends Expected
    ? Actual extends undefined // If only undefined stop here, because NonNullable<undefined> = never
      ? undefined
      : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>
    : never // Actual can be undefined but not Expected: forbid the field
  : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>;

type NonNullableExact<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Actual[K] extends (infer ActualElement)[]
      ? Expected[K] extends (infer ExpectedElement)[] | undefined | null
        ? Exact<ExpectedElement, ActualElement>[]
        : never // Not both array
      : Exact<Expected[K], Actual[K]>
    : never; // Forbid extra properties
};

детская площадка

Exact будет нам очень полезен при возврате ответов API. В настоящее время мы решаем:

const response = { companies };

res.json(exact<GetCompaniesResponse, typeof response>(response));
export function exact<S, T>(object: Exact<S, T>) {
  return object;
}

Здесь тип Exact - это то, что @ArnaudBarre предоставил выше.

Спасибо @ArnaudBarre за то, что разблокировал меня и научил меня некоторым ts.
Рифф от вашего решения:

export type Exact<Expected, Actual> =
  keyof Expected extends keyof Actual
    ? keyof Actual extends keyof Expected
      ? Expected extends ExactElements<Expected, Actual>
        ? Expected
        : never
      : never
    : never;

type ExactElements<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Expected[K] extends Actual[K]
      ? Actual[K] extends Expected[K]
        ? Expected[K]
        : never
      : never
    : never
};

// should succeed (produce exactly the Expected type)
let s1: Exact< { a: number; b: string }, { a: number; b: string } >;
let s2: Exact< { a?: number; b: string }, { a?: number; b: string } >;
let s3: Exact< { a?: number[]; b: string }, { a?: number[]; b: string } >;
let s4: Exact< string, string >;
let s5: Exact< string[], string[] >;
let s6: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string }[] >;

// should fail (produce never)
let f1: Exact< { a: string; b: string }, { a: number; b: string } >;
let f2: Exact< { a: number; b: string }, { a?: number; b: string } >;
let f3: Exact< { a?: number; b: string }, { a: number; b: string } >;
let f4: Exact< { a: number[]; b: string }, { a: string[]; b: string } >;
let f5: Exact< { a?: number[]; b: string }, { a: number[]; b: string } >;
let f6: Exact< { a?: number; b: string; c: string }, { a?: number; b: string } >;
let f7: Exact< { a?: number; b: string }, { a?: number; b: string; c: string } >;
let f8: Exact< { a?: number; b: string; c?: string }, { a?: number; b: string } >;
let f9: Exact< { a?: number; b: string }, { a?: number; b: string; c?: string } >;
let f10: Exact< never, string >;
let f11: Exact< string, never >;
let f12: Exact< string, number >;
let f13: Exact< string[], string >;
let f14: Exact< string, string[] >;
let f15: Exact< string[], number[] >;
let f16: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string } >;

Предыдущее решение «удалось» для f6, f8 и f9.
Это решение также возвращает «более чистые» результаты; когда он совпадает, вы возвращаете тип «Ожидаемый».
Как и в случае с комментарием

@heystewart Ваш Exact не дает симметричного результата:

let a: Exact< { foo: number }[], { foo: number, bar?: string }[] >;
let b: Exact< { foo: number, bar?: string }[], { foo: number }[] >;

a = [{ foo: 123, bar: 'bar' }]; // error
b = [{ foo: 123, bar: 'bar' }]; // no error

Изменить: версия @ArnaudBarre также имеет ту же проблему

@papb Да, по сути, мой набор текста не работает, точка входа - это массив. Мне это было нужно для нашего API-интерфейса graphQL, где variables всегда является объектом.

Чтобы решить эту проблему, вам нужно изолировать ExactObject и ExactArray и иметь точку входа, которая переходит в одно или другое.

Итак, как лучше всего убедиться, что этот объект имеет точные свойства, ни меньше, ни больше?

@ captain-yossarian убедить команду TypeScript реализовать это. Ни одно из представленных здесь решений не работает для всех ожидаемых случаев, и почти во всех из них отсутствует ясность.

@toriningen не может представить, сколько проблем будет закрыто, если команда TS реализует эту функцию

@RyanCavanaugh
В настоящее время у меня есть один пример использования, который привел меня сюда, и он напрямую связан с вашей темой «Разное». Мне нужна функция, которая:

  1. принимает параметр, который реализует интерфейс с необязательными параметрами
  2. возвращает объект, типизированный для более узкого фактического интерфейса данного параметра, так что

Эти непосредственные цели служат следующим целям:

  1. Я получаю лишнюю проверку свойств для ввода
  2. Я получаю автоматическое завершение и типобезопасность свойств для вывода

Пример

Я свел свой случай к следующему:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function y<
    Y extends X
>(
    y: (X extends Y ? Y : X)
) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

Эта установка работает и достигает всех желаемых целей, так что с точки зрения чистой осуществимости и в том, что касается этого, я в порядке. Однако EPC достигается за счет ввода параметра (X extends Y ? Y : X) . Я в основном наткнулся на это случайно и был несколько удивлен, что это сработало.

Предложение

И именно поэтому я хотел бы иметь ключевое слово implements которое можно использовать вместо extends , чтобы отметить намерение, что тип здесь не должен иметь лишних свойств. Вот так:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function x<
    Y implements X
>( y: Y ) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

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

Это также может обеспечить дальнейшие варианты использования, которые в настоящее время невозможны или непрактичны, но в настоящее время это только внутреннее ощущение.

Обнаружение слабого типа как альтернатива

Примечательно, что обнаружение слабого типа согласно # 3842 также должно исправить это и может быть благоприятным из-за того, что не требует дополнительного синтаксиса, если оно работает в сочетании с extends , в соответствии с моим вариантом использования.

Относительно Exact<Type> и т. Д.

Наконец, implements , как я это себе представляю, должен быть довольно прямолинейным в отношении вашей точки зрения о function f<T extends Exact<{ n: number }>(p: T) поскольку он не пытается решить более общий случай Exact<Type> .

Как правило, Exact<Type> , по-видимому, мало полезен по сравнению с EPC, и я не могу представить себе действительный общеполезный случай, выходящий за рамки этих групп:

  • вызовы функций: теперь с ними можно легко справиться, как в моем примере, и они выиграют от implements
  • присваивания: просто используйте литералы, поэтому применяется EPC
  • данные из-за пределов вашей области управления: проверка типов не может защитить вас от этого, вы должны обрабатывать это во время выполнения, после чего вы вернетесь к безопасному приведению

Очевидно, что будут случаи, когда вы не можете назначать литералы, но они также должны быть конечного набора:

  • если вам даны данные о назначении в функции, обработайте проверку типа в сигнатуре вызова
  • если вы объединяете несколько объектов в соответствии с OP, затем правильно утверждаете тип каждого исходного объекта, и вы можете безопасно использовать as DesiredType

Резюме: implements было бы неплохо, но в остальном мы хороши

Таким образом, я уверен, что с помощью implements и исправления EPC (если и когда возникают проблемы) действительно следует обрабатывать точные типы.

Вопрос ко всем заинтересованным сторонам: действительно ли здесь что-нибудь открыто?

Рассмотрев здесь варианты использования, я думаю, что почти все репродукции к настоящему времени обработаны должным образом, а остальное можно заставить работать с моим маленьким примером, приведенным выше. Возникает вопрос: есть ли сегодня у кого-нибудь проблемы с обновленной TS?

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

точно равны, т.е. не больше и не меньше:

function foo(p:{|x:any,y:any|})

//it matched 
foo({x,y})
//no match
foo({x})
foo({y})
foo({x,y,z})
foo({})

больше, но не меньше:

function foo(p:{|x:any,y:any, ...|})

//it matched 
foo({x,y})
foo({x,y,z})

//no matched
foo({x})
foo({y})
foo({x,z})

не больше, но меньше:

function foo(p:{x:any,y:any})

//it matched 
foo({x,y})
foo({x})
foo({y})

//no match
foo({x,z})
foo({x,y,z})

более менее:

function foo(p:{x:any,y:any, ...})

//it matched 
foo({x,y})
foo({x})
foo({y})
foo({x,z})
foo({x,y,z})

заключение:

Вертикальной чертой означает, что нет меньше, без вертикальной линии означает, что может быть меньше. Знак многоточия означает, что может быть больше, без знака многоточия означает, что больше не может быть. Совпадение массивов - та же идея.

function foo(p:[|x,y|]) // p.length === 2
function foo(p:[|x,y, ... |]) // p.length >= 2
function foo(p:[x,y]) // p.length >= 0
function foo(p:[x,y,...]) // p.length >= 0

@rasenplanscher, используя ваш пример, это компилирует:

const x = { blue: 1, red: 3, purple: 4 };
const z = y(x);

Однако с точными типами этого не должно быть. Т.е. здесь прошу не зависеть от EPC.

@ xp44mm «больше, но не меньше» - это уже поведение, а «более или менее» - это поведение, если вы отметите все свойства как необязательные

function foo(p:{x?: any, y?: any}) {}
const x = 1, y = 1, z = 1
// all pass
foo({x,y})
foo({x})
foo({y})
const p1 = {x,z}
foo(p1)
const p2 = {x,y,z}
foo(p2)

Точно так же, если бы у нас были точные типы, точный тип + все необязательные свойства были бы по существу «не больше, но меньше».

Еще один пример к этому вопросу. Думаю, это хорошая демонстрация этого предложения. В этом случае я использую rxjs для работы с Subject, но хочу вернуть ("заблокированный") Observable (у которого нет метода next , error и т. Д. Для управления объектом ценить.)

someMethod(): Observable<MyType> {
  const subject = new Subject<MyType>();

  // This works, but should not. (if this proposal is implemented.)
  return subject;

  // Only Observable should be allowed as return type.
  return subject.asObservable();
}

Я всегда хочу возвращать только точный тип Observable а не Subject который его расширяет.

Предложение:

// Adding exclamation mark `!` (or something else) to match exact type. (or some other position `method(): !Foo`, ...)
someMethod()!: Observable<MyType> {
  // ...
}

Но я уверен, что у вас есть идеи получше. Тем более, что это влияет не только на возвращаемые значения, верно? Во всяком случае, просто демонстрация псевдокода. Думаю, это было бы неплохо, чтобы избежать ошибок и недостатков. Как и в случае, описанном выше. Другим решением может быть добавление нового типа утилиты .
Или я что-то упустил? Это уже работает? Я использую TypeScript 4.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги