Почему TypeScript не исключаетисключить?Это ошибка? - PullRequest
0 голосов
/ 04 июля 2019

Это сложно объяснить, но я пытаюсь отфильтровать определенный тип в подполе типа объекта.

Exclude<T, U>, похоже, не работает с таким типом вложенности:

type PrivateName = { kind: "PrivateName" };
type Identifier = { kind: "Identifier" };

type NamedNode = {
  name: PrivateName | Identifier;
};

type PrivateNamedNode = {
  name: PrivateName;
};

declare const privateName: PrivateName;

type NamedNodeNonPrivate = Exclude<NamedNode, PrivateNamedNode>;

// I expected this next line to error, but it doesn't!
const x: NamedNodeNonPrivate = { name: privateName };

Почему не произошла ошибка последней строки? Должен ли я открыть отчет об ошибке?

Проверено на версиях TS: 3.5.1, 3.3.3, 3.1.6 и 3.0.1. ссылка на игровую площадку .

Потенциальная первопричина

Определение Exclude<T, U> равно

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

В моем примере это кажется инертным, потому что NamedNode не распространяется { name: PrivateName }.

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

type NamedNode = {
  name: PrivateName
} | {
  name: Identifier
}

Изоморфизм Карри-Говарда говорит нам, что типы произведений подобны соединению, а типы сумм подобны дизъюнкции.

В булевой логике верно следующее:

A & (B or C) implies (A & B) or (A & C)

Так что кажется разумным, что два определения NamedNode будут эквивалентны. Это отсутствие дистрибутивности:

  • 1 причина моей проблемы выше
  • 2 ошибка?

1 Ответ

1 голос
/ 04 июля 2019

{ name: PrivateName | Identifier; }; и { name: PrivateName } | { name: Identifier } эквивалентны только в тривиальном случае, когда там тип объекта содержит только это свойство. Обычный вариант использования состоит в том, что разные ветви объединения будут содержать разные типы.

Независимо от этого условные типы распределяют , но только по параметрам «голого» типа. Никакого волшебного распределения не происходит по свойствам каждого типа. И поскольку отношение NamedNode extends PrivateNamedNode не является истинным, никакие типы не будут исключены (ни один тип не может быть исключен, поскольку NamedNode не является объединением, даже если отношение было истинным, вы получите never, поскольку тогда весь тип будет исключено, и вы останетесь ни с чем).

Мы можем создать тип, исключающий типы из свойств объекта:

type PrivateName = { kind: "PrivateName" };
type Identifier = { kind: "Identifier" };

type NamedNode = {
    name: PrivateName | Identifier;
};

type PrivateNamedNode = {
    name: PrivateName;
};

declare const privateName: PrivateName;

type ExcludePropertyTypes<T, TExclude extends Partial<T>> {
    [P in keyof T]: Exclude<T[P], TExclude[P]>
}

type NamedNodeNonPrivate = ExcludePropertyTypes<NamedNode, PrivateNamedNode>;

// Errors now (as intended)
const x: NamedNodeNonPrivate = { name: privateName };

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

...