Поддерживает ли Typescript типы подмножеств?

Допустим, у меня есть интерфейс:

interface IUser {
  email: string;
  id: number;
  phone: string;
};

Затем у меня есть функция, которая ожидает подмножество (или полное совпадение) этого типа. Может быть, он пропустит весь объект, а просто передаст {email: "[email protected]"}. Я хочу, чтобы проверка типов позволила им обоим.

Пример:

function updateUser(user: IUser) {
  // Update a "subset" of user attributes:
  $http.put("/users/update", user);
}

Поддерживает ли Typescript такого рода поведение? Я мог бы найти это очень полезным, особенно с такими парадигмами, как Redux.

Чтобы уточнить, цель:

  1. Избегайте переписывания интерфейса и ручной настройки всех атрибутов на необязательные.
  2. Избегайте присвоения неожиданных атрибутов (таких как орфографические ошибки).
  3. Избегайте императивной логики, такой как оператор if, которая лишается преимуществ проверки типов времени компиляции.

ОБНОВЛЕНИЕ: Typescript объявил о поддержке отображаемых типов, которые должны решить эту проблему после публикации.

Ответ 1

Typescript теперь поддерживает частичные типы.

Правильный способ создания частичного типа:

type PartialUser = Partial<IUser>;

Ответ 2

Вы можете объявить некоторые или все поля в качестве необязательных полей.

interface IUser {
  email: string; // not optional
  id?: number; // optional 
  phone?: string; // optional
};

Ответ 3

правильное решение с отображенными типами:

updateUser<K extends keyof IUser>(userData: {[P in K]: IUser[P]}) {
    ...
}

Ответ 4

Вы можете разделить его на разные интерфейсы:

interface IUser {
    id: number;
};

interface IUserEmail extends IUser {
    email: string;
}

interface IUserPhone extends IUser {
    phone: string;
}

Попросите ваш метод получить базовый интерфейс IUser, а затем проверьте нужные поля:

function doit(user: IUser) {
    if (user.email) {

    } else if (user.phone) {

    }
}

Ответ 5

Если я правильно понял этот вопрос, вам нужно что-то вроде Flow $Shape

Итак, в одном месте у вас может быть что-то, что требует типа

interface IUser {
  email: string;
  id: number;
  phone: string;
};

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

interface IUserOptional {
  email?: string;
  id?: number;
  phone?: string;
};

Вам нужен способ автоматического генерации IUserOptional на основе IUser без необходимости повторного вывода типов.

Теперь я не думаю, что это возможно в Typescript. Вещи могут измениться в 2.0, но я не думаю, что мы еще ближе к чему-то вроде этого в Typescript.

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

С учетом этой проблемы я могу предложить только попробовать вместо Flow. В потоке вы можете просто сделать $Shape<IUser>, чтобы сгенерировать тип, который вы хотите программно. Конечно, Flow отличается от Typescript многими большими и малыми способами, поэтому имейте это в виду. Поток не является компилятором, поэтому вы не получите таких вещей, как Enums и класс, реализующие интерфаки

Ответ 6

Что вы хотите это

type Subset<T extends U, U> = U;

это гарантирует, что U является подмножеством T и возвращает U как новый тип. например:

interface Foo {
 name: string;
 age: number;
}

type Bar = Subset<Foo, {
 name: string;
}>;

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

Ответ 7

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

Если вы хотите сделать некоторые поля обязательными (например, id и email), вам нужно объединить их с Pick:

type UserWithOptionalPhone = Pick<IUser, 'id' | 'email'> & Partial<IUser>

Некоторое объяснение:

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

function hello1(user: Pick<IUser, 'id' | 'email'>) {
}

hello1({email: '@', id: 1}); //OK

hello1({email: '@'}); //Not OK, id missing

hello1({email: '@', id: 1, phone: '123'}); //Not OK, phone not allowed

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

function hello2(user: Pick<IUser, 'id' | 'email'> & Partial<IUser>) {
}

hello2({email: '@', id: 1}); //OK

hello2({email: '@', id: 1, phone: '123'}); //OK

hello2({email: '@'}); //Not OK, id missing