1. Дискриминантное объединение (Discriminated Union)
Тип Discriminated Unions (дискриминантное объединение), часто обозначаемое как Tagged Union (размеченное объединение), так же как и тип union (объединение), является множеством типов, перечисленных через прямую черту | . Значение, ограниченное дискриминантным объединением, может принадлежать только к одному типу из множества.
Несмотря на то, что Discriminated Union в большей степени идентичен типу Union , все же существует два отличия.
Первое отличие заключается в том, что типу Discriminated Union могут принадлежать только ссылочные типы данных.
Второе отличие в том, что каждому объектному типу, также называемые варианты, составляющему Discriminated Union , указывается идентификатор варианта который называется дискриминант.
Помните, что вывод типов, без помощи разработчика, способен работать лишь с общими для всех типов признаками?
Рассмотрим пример:
class Bird {
fly(): void {}
toString(): string {
return 'bird';
}
}
class Fish {
swim(): void {}
toString(): string {
return 'fish';
}
}
class Insect {
crawl(): void {}
toString(): string {
return 'insect';
}
}
Поскольку вывод типов не может определить к какому конкретно из трех типов принадлежит параметр animal, он не позволяет обращаться к уникальным для каждого типа членам, коими являются методы fly, swim, crawl.
В отличии от этих методов, метод toString определен в каждом из возможных типов, поэтому при его вызове ошибки не возникает:
function move(animal: Bird | Fish | Insect): void {
animal.fly(); // Error -> [*]
animal.swim(); // Error -> [*]
animal.crawl(); // Error -> [*]
animal.toString(); // Ok -> [*]
}
1.1 Сужение на основе признаков присущих типу Tagged Union
Чтобы компилятор мог работать с членами присущих конкретным типам, составляющих дискриминантное объединение, одним из способов является сужение диапазона типов при помощи так называемого дискриминанта.
class Bird {
type: 'bird' = 'bird'; // дискриминант
fly(): void {}
toString(): string {
return 'bird';
}
}
class Fish {
type: 'fish' = 'fish'; // дискриминант
swim(): void {}
toString(): string {
return 'fish';
}
}
class Insect {
type: 'insect' = 'insect'; // дискриминант
crawl(): void {}
toString(): string {
return 'insect';
}
}
Используя type литеральный тип (можно использовать и другое название), вы можете сравнить значение type с эквивалентной строкой, и TypeScript будет знать, какой тип используется в данный момент.
function move(animal: Bird | Fish | Insect): void {
if (animal.type === 'bird')
animal.fly(); // Ok
else if (animal.type === 'fish')
animal.swim(); // Ok
else
animal.crawl(); // Ok
animal.toString(); // Ok
}
Прежде всего стоит прояснить, что дискриминант, это поле, которое обязательно должно принадлежать к литеральному типу отличному от unique symbol и определенное в каждом типе, составляющем дискриминантное объединение. Кроме того, поля обязательно должны быть инициализированы при объявлении или в конструкторе.
В случае, когда типы полей являются уникальными для всего множества, они идентифицируют только свой тип.
class Bird {
groupID: 0 = 0;
fly(): void {}
}
class Fish {
groupID: 1 = 1;
swim(): void {}
}
class Insect {
groupID: 2 = 2;
crawl(): void {}
}
// groupID 0 === Bird
// groupID 1 === Fish
// groupID 2 === Insect
Тогда, когда тип поля не является уникальным, он идентифицирует множество типов, у которых совпадают типы одноимённых идентификаторов вариантов.
class Bird {
groupID: 0 = 0;
fly(): void {}
}
class Fish {
groupID: 0 = 0;
swim(): void {}
}
class Insect {
groupID: 1 = 1;
crawl(): void {}
}
// groupID 0 === Bird | Fish
// groupID 1 === Insect
Количество полей, которые служат идентификаторами вариантов, может быть любым.
При необходимости декларирования поля, выступающего в роли дискриминанта, в интерфейсе, ему указывается более общий совместимый тип. Для литерального строкового типа, это тип string , для литерального числового, это number и т.д.
interface IT {
/**
* Дискриминантное поле
*/
type: string; // это поле предполагается использовать в качестве дискриминанта
}
class A implements IT {
type: "a" = "a"; // переопределение более конкретным типом
}
class B implements IT {
type: "b" = "b"; // переопределение более конкретным типом
}
2. Сужение дипазона типов (Narrowing)
Представьте, что у нас есть функция под названием padLeft. Если padding это number, он будет рассматривать это как количество пробелов, которые мы хотим добавить к input. Если paddingэто string, он должен просто добавить paddingк input. Давайте попробуем реализовать логику для случая, когда padLeft передается как number padding.
function padLeft(padding: number | string, input: string): string {
return " ".repeat(padding) + input;
}
Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.
Дело в том, что компилятор TypeScript не позволит передать в метод repeat padding, поскольку для него идентификатор padding, принадлежит к типу number ровно настолько же, насколько он принадлежит к типу string . TypeScript предупреждает нас, что мы передаём значение с типом number | string в repeatфункцию, которая принимает только number, и это верно. Другими словами, мы явно не проверили , paddingявляется ли онnumber, и не обрабатываем ли мы случай, когда он отностится к string, так что давайте сделаем именно это.
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Если это по большей части выглядит как неинтересный код JavaScript, то в этом-то и суть. За исключением добавленных нами аннотаций, этот код TypeScript выглядит как JavaScript. Идея заключается в том, что система типов TypeScript призвана максимально упростить написание типичного кода JavaScript, не утруждая себя обеспечением типобезопасности.
Именно из-за этой особенности или другими словами, неоднозначности, которую вызывает тип Union , в TypeScript, появился механизм называемый защитниками типа (Type Guards) который рассматривается далее.
Защитники типа — это правила, которые помогают выводу типов определить суженый диапазон типов для значения, принадлежащего к типу Union . Другими словами, разработчику предоставлен механизм, позволяющий с помощью выражений составить логические условия, проанализировав которые, вывод типов сможет сузить диапазон типов до указанного и выполнить над ним требуемые операции.
Хотя это может показаться не таким уж сложным, на самом деле здесь происходит много интересного. Подобно тому, как TypeScript анализирует значения во время выполнения, используя статические типы, он накладывает анализ типов на конструкции управления потоком выполнения JavaScript, такие как if/else, условные тернарные операторы, циклы, проверки истинности и т. д., которые могут влиять на эти типы.
В рамках нашей if проверки TypeScript распознаёт typeof padding === "number"и распознаёт это как особую форму кода, называемую «охраной типа» (type guard) .
TypeScript отслеживает возможные пути выполнения, которые могут использовать наши программы, чтобы проанализировать наиболее точный тип значения в заданной позиции. Он учитывает эти специальные проверки (называемые «охраной типа») и присваивания, а процесс уточнения типов до более конкретных, чем объявленные, называется «сужением» (substruction).
Для того, что бы облегчить работу компилятору, TypeScript предлагает процесс сужения множества типов, составляющих тип Union , до заданного диапазона, а затем закрепляет его за конкретной областью видимости в коде. Но, прежде чем диапазон типов будет вычислен и ассоциирован с областью, разработчику необходимо составить условия, включающие в себя признаки, недвусмысленно указывающие на принадлежность к нужным типам.
Из-за того, что анализ происходит на основе логических выражений, область, за которой закрепляется суженый диапазон типов, ограничивается областью выполняемой при истинности условия. Стоит заметить, что от признаков, участвующих в условии, зависит место, в котором может находится выражение, а от типов, составляющих множество типа Union , зависит способ составления логического условия.
2.1 Анализ потока управления (Control flow analysis)
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Обратите внимание, что функция padLeftвозвращается из своего первого ifблока. TypeScript смог проанализировать этот код и увидеть, что остальная часть тела (строка 6) недоступна в случае, когда padding является number. В результате удалось удалить numberиз типа padding(сужаясь с string | numberдо string) для оставшейся части функции.
Этот анализ кода, основанный на достижимости, называется анализом потока управления, и TypeScript использует его для сужения типов при обнаружении защитников типа. При анализе переменной, поток управления может разделяться и объединяться снова и снова, и можно наблюдать, что эта переменная имеет другой тип в каждой точке выполнения.
Далее будут рассмотрены варианты сужений диапазона множества типов с помощью различных механизмов защитников типа.
2.2 Сужение диапазона множества типов на основе типа данных
2.2.1 typeof сужение
Как мы видели, JavaScript поддерживает typeofоператор, который может предоставить самую базовую информацию о типе значений, имеющихся во время выполнения. TypeScript ожидает, что он вернет определённый набор строк:
"string""number""bigint""boolean""symbol""undefined""object""function"
При необходимости составления условия, в основе которого лежат допустимые с точки зрения JavaScript типы, прибегают к помощи уже знакомых операторов как typeof и instanceof .
К помощи оператора typeof прибегают тогда, когда хотят установить принадлежность к типам number , string , boolean , object , function , symbol или undefined . Если значение принадлежит к производному от объекта типу, то установить его принадлежность к типу определяемого классом и находящегося в иерархии наследования, можно при помощи оператора instanceof .
С помощью операторов typeof и instanceof составляется условие по которому компилятор может вычислить к какому конкретно типу или диапазону будет относиться значение в определяемой условием области.
// Пример для оператора typeof
type ParamType = number | string | boolean | object | Function | symbol | undefined;
function identifier(param: ParamType): void {
param; // param: number | string | boolean | object | Function | symbol | undefined
if (typeof param === 'number') {
param; // param: number
}
else if (typeof param === 'string') {
param; // param: string
}
else if (typeof param === 'boolean') {
param; // param: boolean
}
else if (typeof param === 'object') {
param; // param: object
}
else if (typeof param === 'function') {
param; // param: Function
}
else if (typeof param === 'symbol') {
param; // param: symbol
}
else if (typeof param === 'undefined') {
param; // param: undefined
}
param; // param: number | string | boolean | object | Function | symbol | undefined
}
2.2.2 instanceof сужение
В JavaScript есть оператор для проверки того, является ли значение «экземпляром» другого значения. В частности, в JavaScript оператор x instanceof Foo проверяет, содержится ли x в цепочке прототипов Foo.
// Пример для оператора instanceof
class Animal {
constructor(public type: string) {}
}
class Bird extends Animal {}
class Fish extends Animal {}
class Insect extends Animal {}
function f(param: Animal | Bird | Fish | Insect): void {
param; // param: Animal | Bird | Fish | Insect
if (param instanceof Bird) {
param; // param: Bird
}
else if (param instanceof Fish) {
param; // param: Fish
}
else if (param instanceof Insect) {
param; // param: Insect
}
param; // param: Animal | Bird | Fish | Insect
}
Если значение принадлежит к типу Union , а выражение состоит из двух операторов, if и else , значение находящиеся в операторе else будет принадлежать к диапазону типов не участвующих в условии if.
function f0(param: number | string | boolean): void {
param; // param: number | string | boolean
if (typeof param === 'number' || typeof param === 'string') {
param; // param: number | string
}
else {
param; // param: boolean
}
param; // param: number | string | boolean
}
Кроме того, условия можно поместить в тернарный оператор. В этом случае область на которую распространяется сужение диапазона типов, ограничивается областью содержащей условное выражение.
function f(param: string | (() => string)): void {
param; // param: string | (() => string)
let value: string = typeof param !== 'string' ? param() : param;
param; // param: string | (() => string)
}
Так как оператор switch логически похож на оператор if / else , то может показаться, что механизм, рассмотренный здесь, будут применимы и к нему. Но это не так. Вывод типов не умеет различать условия составленные при помощи операторов typeof и instanceof в конструкции switch .
2.2.3 Оператор in сужения
В JavaScript есть оператор, определяющий, есть ли у объекта или его цепочки прототипов свойство с именем: in оператор. TypeScript учитывает это, чтобы сузить круг потенциальных типов. Другими словами Сужение диапазона типов также возможно на основе доступных ( public ) членов, присущих типам, составляющим диапазон ( Union ). Сделать это можно с помощью оператора in .
Например, с кодом: "value" in x. где "value"— строковый литерал, а x— тип объединения. Ветвь «истина» сужает xтипы , имеющие либо необязательное, либо обязательное свойство value, а ветвь «ложь» сужает типы, имеющие необязательное или отсутствующее свойство value.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal)
return animal.swim();
return animal.fly();
}
необязательные свойства будут присутствовать в обеих сторонах для сужения.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal)
animal; // animal: Fish | Human
else
animal; // animal: Bird | Human
}
2.3 Сужение истины (Truthiness narrowing)
Возможно, слово «истина» вы не найдете в словаре, но вы часто услышите о нем в JavaScript.
В JavaScript мы можем использовать любое выражение в условных операторах, операторах &&, ||, if, булевых отрицаниях (!) и т. д. Например, ifоператоры не ожидают, что их условие всегда будет иметь тип boolean.
В JavaScript такие конструкции, как , if сначала «приводят» свои условия к booleans, чтобы сделать их осмысленными, а затем выбирают свои ветви в зависимости от того, является ли результат true или false.
Вы всегда можете привести значения к booleans, пропустив их через Boolean функцию или используя более короткое двойное логическое отрицание. (Последний вариант имеет преимущество в том, что TypeScript выводит узкий литеральный логический тип true, в то время как первый выводит как тип boolean)
Такое поведение довольно популярно, особенно для защиты от значений типа null или undefined. В качестве примера попробуем использовать его для нашей printAll функции.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object")
for (const s of strs)
console.log(s);
else if (typeof strs === "string")
console.log(strs);
}
Вы заметите, что мы избавились от ошибки, указанной выше, проверив strsистинность. Это, по крайней мере, предотвращает возникновение опасных ошибок при запуске нашего кода, например:
TypeError: null is not iterable
Однако помните, что проверка истинности примитивов часто может быть подвержена ошибкам. В качестве примера рассмотрим другую попытку написанияprintAll
function printAll(strs: string | string[] | null) {
if (strs)
if (typeof strs === "object")
for (const s of strs)
console.log(s);
else if (typeof strs === "string")
console.log(strs);
}
Мы заключили все тело функции в проверку истинности, но у этого есть небольшой недостаток: мы можем больше не обрабатывать случай пустой строки правильно.
TypeScript здесь совершенно не вредит, но на это поведение стоит обратить внимание, если вы не очень хорошо знакомы с JavaScript. TypeScript часто помогает выявлять ошибки на ранних этапах, но если вы решите ничего не делать со значением, то его возможности ограничены, если не быть чрезмерно предписывающими. При желании вы можете обеспечить обработку подобных ситуаций с помощью линтера.
Последнее слово об сужении по истинности: булевские отрицания !отфильтровываются из отрицаемых ветвей.
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
2.4 Сужение равенства (Equality narrowing)
TypeScript также использует switchоператоры и проверки равенства, такие как ===, !==, ==, и , !=для ограничения типов. Например:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// Теперь мы можем вызвать любой метод «string» для «x» или «y».
x.toUpperCase(); // (method) String.toUpperCase(): string
y.toLowerCase(); // (method) String.toLowerCase(): string
}
else {
x // (parameter) x: string | number
y // (parameter) y: string | boolean
}
}
Когда мы проверили, что xи yоба равны в примере выше, TypeScript знал, что их типы также должны быть равны. Поскольку stringэто единственный общий тип, который могут принимать оба x и y, TypeScript это знает что x и y будут string в первой ветке.
Проверка по конкретным литеральным значениям (в отличие от переменных) также работает. Более свободные проверки на равенство в JavaScript с помощью ==и !=также корректно сужаются. Если вы не знакомы, проверка на == nullдействительность не только проверяет, является ли что-то значением null, но и потенциально является undefined. То же самое относится и к == undefined: она проверяет, является ли значение либо , null либо undefined.
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value) // (property) Container.value: number
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
2.5 Использование предикатов типа (type predicates)
Все перечисленные ранее способы работают только в том случае, если проверка происходит в месте отведенном под условие. Другими словами, с помощью перечисленных до этого момента способов, условие проверки нельзя вынести в отдельный блок кода (функцию). Это могло бы сильно ударить по семантической составляющей кода, а также нарушить принцип разработки программного обеспечения, который призван бороться с повторением кода (Don’t repeat yourself, DRY (не повторяйся)). Но, к счастью для разработчиков, создатели TypeScript реализовали возможность определять пользовательские защитники типа.
В роли пользовательского защитника может выступать функция, функциональное выражение или метод, которые обязательно должны возвращать значения, принадлежащие к типу boolean . Для того, что бы вывод типов понял, что вызываемая функция не является обычной функцией, у функции вместо типа возвращаемого значения указывают предикат типа (type predicates).
Предикат — это логическое выражение, значение которого может быть либо истинным true , либо ложным false. Выражение предиката состоит из трех частей и имеет следующий вид: identifier is Type .
Первым членом выражения является идентификатор, который обязан совпадать с идентификатором одного из параметров объявленных в сигнатуре функции. В случае, когда предикат указан методу экземпляра класса, в качестве идентификатора может быть указано ключевое слово this . Ключевое слово this можно указать только в сигнатуре метода, определенного в классе или описанного в интерфейсе. При попытке указать ключевое слово this в предикате функционального выражения, не получится избежать ошибки, если это выражение определяется непосредственно в prototype , функции конструкторе, либо методе объекта, созданного с помощью литерала.
Ко второму члену выражения относится ключевое слово is , которое служит в качестве утверждения. В качестве третьего члена выражения может выступать любой тип данных.
// Пример предиката функции (function declaration)
function isString(p: any): p is string {
return typeof p === 'string'
}
// Пример предиката функционального выражения (functional expression)
const isString = (p: any): p is string => typeof p === 'string'
// Пример предиката метода класса (static method)
class Validator {
public static isString(p: string | number): p is string {
return typeof p === 'string'
}
}
Условие, на основании которого разработчик определяет принадлежность одного из параметров к конкретному типу данных, не ограничено никакими конкретными правилами. Исходя из результата выполнения условия true или false , вывод типов сможет установить принадлежность указанного параметра к указанному типу данных.
Как вы могли заетить, для того чтобы определить пользовательскую защиту типа, нам просто нужно определить функцию, тип возврата которой является предикатом типа:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
pet is Fish— это наш предикат типа в этом примере. При каждом isFishвызове с некоторой переменной TypeScript сузит тип этой переменной до определенного типа, если исходный тип совместим.
let pet = getSmallPet();
if (isFish(pet))
pet.swim();
else
pet.fly();
Обратите внимание, что TypeScript не только знает, что в ветке petесть, он также знает, чего в ветке у вас нет.
Вы можете использовать защиту типа isFishдля фильтрации массива Fish | Birdи получения массива Fish:
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// или, что то же самое
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
// Для более сложных примеров предикат может потребовать повторения.
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "sharkey")
return false;
return isFish(pet);
});
Кроме того, классы могут использовать this is Type выражение для сужения своего типа.
Можно использовать this is Typeв позиции возврата для методов классов и интерфейсов. В сочетании с сужением типа (например, ifоператорами) тип целевого объекта будет сужен до указанного значения Type.
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
interface Networked {
host: string;
}
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
if (fso.isFile())
fso.content; // const fso: FileRep
else if (fso.isDirectory())
fso.children; // const fso: Directory
else if (fso.isNetworked())
fso.host; // const fso: Networked & FileSystemObject
Типичный пример использования защиты типа this — ленивая проверка конкретного поля. Например, в этом случае удаляется undefined из значения, хранящегося в поле, после того, как hasValue оно будет проверено как истинное:
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box<string>();
box.value = "Gameboy";
box.value; // (property) Box<string>.value?: string
if (box.hasValue())
box.value; // (property) value: string
2.6 Использование функции утверждения (Assertion functions)
Существует определённый набор функций, которые throw выдают ошибку в случае непредвиденных обстоятельств. Они называются функциями «assertion». Например, в Node.js для этого есть специальная функция, называемая assert.
assert(someValue === 42);
В этом примере, если someValueне равно 42, то assertбудет выдано исключение AssertionError.
Утверждения в JavaScript часто используются для защиты от передачи неправильных типов. Например:
function multiply(x, y) {
assert(typeof x === "number");
assert(typeof y === "number");
return x * y;
}
К сожалению, в TypeScript эти проверки никогда не могли быть корректно закодированы. Для слабо типизированного кода это означало, что TypeScript выполнял меньше проверок, а для более консервативного кода часто вынуждал пользователей использовать утверждения типов.
function yell(str) {
assert(typeof str === "string");
return str.toUppercase();
// Упс! Мы неправильно написали «toUpperCase».
// Было бы здорово, если бы TypeScript всё ещё это распознавал!
}
Альтернативой было бы переписать код так, чтобы язык мог его проанализировать, но это неудобно.
function yell(str) {
if (typeof str !== "string")
throw new TypeError("str should have been a string.");
// Обнаружена ошибка!
return str.toUppercase();
}
Конечная цель TypeScript — типизировать существующие конструкции JavaScript наименее разрушительным способом. Поэтому в TypeScript 3.7 введена новая концепция, называемая «сигнатурами утверждений», которая моделирует эти функции утверждений.
Для того, что бы объявить утверждающую функцию, в её сигнатуре (там где располагается возвращаемое значение) следует указать ключевое слово asserts , а затем параметр принимаемого на вход условия.
function identifier(condition: any): asserts condition {
if (!condition)
throw new Error('');
}
Ключевой особенностью утверждения в сигнатуре является то, что в качестве аргумента утверждающая функция ожидает выражение, определяющие принадлежность к конкретному типу с помощью любого предназначенного для этого механизма (которые уже были рассмотрены выше).
Если принадлежность значения к указанному типу подтверждается, то далее по коду компилятор будет рассматривать его в роли этого типа. Иначе выбрасывается исключение.
import { AssertionError } from "assert";
// утверждение в сигнатуре
function isStringAssert(condition: any): asserts condition {
if (!condition)
throw new AssertionError({ message: 'value is should be string' })
}
// утверждение типа
function isString(value: any): value is string {
return typeof value === 'string';
}
const testScope = (text: any) => {
text.toUppercase(); // до утверждения расценивается как тип any..
isStringAssert(text instanceof String); // выражение с оператором instanceof
isStringAssert(typeof text === 'string'); // выражение с оператором typeof
isStringAssert(isString(text)); // механизм "утверждения типа"
text.toUpperCase(); // после утверждения, как тип string
}
При использовании механизма утверждения в сигнатуре с механизмом утверждения типа, условие можно перенести из вызова утверждающей функции в её тело. Такой тип сигнатуры утверждения не проверяет условие, а вместо этого сообщает TypeScript, что конкретная переменная или свойство имеет другой тип. Здесь asserts val is string гарантируется, что после любого вызова isStringAsserts любая переданная переменная будет известна как string.
import { AssertionError } from "assert";
function isStringAsserts(value: any): asserts value is string {
if (typeof value !== "string")
throw new AssertionError({ message: 'value is should be string' })
}
const testScope = (text: any) => {
text.toUppercase(); // не является ошибкой, потому, что тип — any
isStringAsserts(text); // условие определено внутри утверждающей функции
text.toUppercase(); // Ошибка, не правильно написали toUpperCase
}
Стоит обратить внимание на то, что механизм утверждения типа не будет работать в случае переноса условного выражения в тело утверждающей функции, сигнатура которой, лишена утверждения типов и содержит исключительно утверждения в сигнатуре.
function isStringAsserts(value: any): asserts value /** is string */ {
if (typeof value !== "string") {
throw new Error(``);
}
}
const testScope = (text: any) => {
text.toUppercase(); // не является ошибкой, потому, что тип — any
isStringAsserts(text); // условие определено в утверждающей функции
text.toUppercase(); // нет ошибки, потому, что утверждение типов не работает
}
Эти сигнатуры утверждений очень похожи на сигнатуры предикатов, и, как и сигнатуры предикатов типов, эти сигнатуры утверждений невероятно функциональны. С их помощью можно выразить довольно сложные идеи.
import { AssertionError } from "assert";
function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
if (val === undefined || val === null)
throw new AssertionError({ message: `Ожидалось, что «val» будет определен, но получено ${val}`})
}