Расширить тип
У меня в одну функцию приходит такой же тип, но + ещё поле. Не хочется создавать новый только ради одной функции.
Можно ли как-то указать, что он расширяется?
1 2 3
function test(arg: User + gender) { }
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
Ответы с готовыми решениями:
Расширить объект number
как реализовать? Расширить объект number новыми свойствами и методами позволяющими задавать единицы.
Расширить функционал сайта
Здравствуйте всем, есть сайт на php, его задача отображать поездки грузовиков в графиках и на.
Как расширить компонент
По сути два вопроса, первый: я использую готовую библиотеку-компонент, не уверен как правильно.
Расширить textarea при клике
Как сделать что б при нажатии на текстовое(textarea) подле оно разворачивалась?(примерно как в.
Регистрация: 31.03.2015
Сообщений: 447
1 2 3
function test (arg: User & { extra: string }) { }
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
Помогаю со студенческими работами здесь
Как эффективно расширить возможности функции
Доброго времени суток. Есть ф-я, ее задача получить список ссылок сайтов и спарсить rss этих.
Расширить решение для чтения rss feed-a
Добрый день. есть вопрос,хочу добавить контент к рсс фиду,есть бот который постит инфу с фида с чат.
Расширить тип данных двусторонняя очередь
Нужна помощь с заданием: Расширьте тип данных двусторонняя очередь (dequeue), добавляя.
Пункт Расширить том в Windows Vista Home Basic не активен — Как же расширить?
Товарищи, здравствуйте. Подскажите, пожалуйста, а как же расширить в Vista Home Basic том C.
Как «расширить» тип integer
Возникла проблема. Во время выполнения программы вылезает ошибка: Fortran runtime error: Attempt to.
Не могу расширить том (на любом диске «расширить том» не активно)
Столкнулся с проблемой. Есть 260 ГБ не распределеной памяти. Но на любом диске "разширить том" не.
Тип объединение #
Иногда Вы будете работать с функцией, ожидающей параметр типа number или string . Например, возьмём следующую функцию:
/** * Принимает строку и добавляет содержимое "padding" в левую сторону выражения. * Если 'padding' - строка, тогда 'padding' добавляется в левую сторону 'value'. * Если 'padding' - число, тогда соответствующее число пробелов добавляется в левую сторону 'value'. */ function padLeft(value: string, padding: any) < if (typeof padding === "number") < return Array(padding + 1).join(" ") + value; >if (typeof padding === "string") < return padding + value; >throw new Error(`Ожидал строку или число, а получил '$'.`); > padLeft("Hello world", 4); // возвращает " Hello world"
Проблема с функцией padLeft в том, что тип параметра padding взят как any . Это означает, что мы можем вызвать его с параметрами, не являющимися number и string , на что TypeScript не выведет никаких ошибок.
let indentedString = padLeft("Hello world", true); // проходит во время компиляции, но не выполнится во время выполнения.
В традиционном объектно-ориентированном коде мы могли бы абстрагироваться над двумя типами созданием иерархии типов. Несмотря на то, что это кажется гораздо более явным, это также немного выходит за рамки дозволенного. Одним из хороших черт оригинальной версии padLeft была возможность передавать только примитивы. Это значит, что использование было простым и не слишком многословным. Новый подход также не помог бы при попытке использовать функцию, которая уже существует где-то.
Вместо any мы можем использовать тип объединение для параметра padding :
/** * Принимает строку и добавляет содержимое "padding" в левую сторону выражения. * Если 'padding' - строка, тогда 'padding' добавляется в левую сторону. * Если 'padding' - число, тогда соответствующее число пробелов добавляется в левую сторону. */ function padLeft(value: string, padding: string | number) < // . >let indentedString = padLeft("Hello world", true); // выведет ошибки во время компиляции
Тип объединение описывает значение, которое может быть одним из нескольких типов. Мы используем вертикальную черту ( | ), чтобы отделить каждый тип, так, number | string | boolean — это тип значения, который может быть числом number , строкой string или булевым boolean .
Если у нас есть значение типа объединения, мы можем получить доступ только к тем его элементам, которые являются общими для всех типов в объединении.
interface Bird < fly(); layEggs(); >interface Fish < swim(); layEggs(); >function getSmallPet(): Fish | Bird < // . >let pet = getSmallPet(); pet.layEggs(); // ок pet.swim(); // ошибка
Тип объединение может быть немного запутан, потребуется немного интуиции, чтобы привыкнуть. Если значение имеет тип A | B , мы знаем только то, что оно имеет только те элементы, которые имеют оба A и B . В этом примере Bird имеет элемент именованный fly . Мы не можем быть уверены в том, есть ли у переменной, имеющей тип Bird | Fish , метод fly . Если переменная во время выполнения на самом деле является Fish , тогда вызов pet.fly() не выполнится.
Защитники типа и различие типов #
Типы объединения полезны для моделирования ситуаций, когда значения могут совпасть в типах, которые они могут приобрести. Что случится, когда нам необходимо узнать: у нас переменная типа Fish ? Обычная идиома в JavaScript для различия между двумя возможными значениями — это проверка на присутствие элемента. Как мы уже упоминали, Вы можете получить доступ только к элементам, которые гарантированно будут во всех составляющих типа объединения.
let pet = getSmallPet(); // Каждый доступ к свойству приведёт к ошибке if (pet.swim) < pet.swim(); >else if (pet.fly)
Чтобы этот код работал, нам нужно воспользоваться утверждением типа (type assertion)
let pet = getSmallPet(); if ((pet).swim) < (pet).swim(); >else
Определённые Пользователем Защитники Типа
Заметьте, что мы должны были использовать утверждения типов несколько раз. Было бы намного лучше, если бы после выполнения проверки мы могли бы знать тип pet внутри каждого ответвления.
Так получилось, что у TypeScript есть что-то, что называется защитник типа. Защитник типа — это некоторое выражение, выполняющее проверку во время выполнения, гарантирующий тип в некоторой области. Чтобы определить защитник типа, нужно просто определить функцию, чей тип возврата является предикатом типа:
function isFish(pet: Fish | Bird): pet is Fish
pet is Fish — это предикат типа. Предикат принимает форму parameterName is Type , где parameterName — имя параметра из текущей сигнатуры функции.
Всякий раз, когда с некоторой переменной вызывается isFish , TypeScript ограничивает эту переменную в специфический тип, при условии, что оригинальный тип совместим.
// Оба вызова, ‘swim’ и ‘fly’, теперь в порядке. if (isFish(pet)) < pet.swim(); >else
Заметьте, что TypeScript не только знает, что pet это Fish в if ветви; он также знает, что в ветви else у Вас не Fish , так что у Вас должно быть так: pet — это Bird .
Защитники типа typeof
В действительности мы не обсуждали реализацию версии padLeft , использовавшую тип объединение. Мы могли бы записать его с предикатами типа как следующее:
function isNumber(x: any): x is number < return typeof x === "number"; >function isString(x: any): x is string < return typeof x === "string"; >function padLeft(value: string, padding: string | number) < if (isNumber(padding)) < return Array(padding + 1).join(" ") + value; >if (isString(padding)) < return padding + value; >throw new Error(`Ожидал строку или число, а получил '$'.`); >
Однако, имея необходимость определить функцию для выяснения примитивности типа — это боль. К счастью, Вам не обязательно абстрагировать typeof x === «number» в свою собственную функцию, потому что TypeScript распознает его как защитник типа самостоятельно. Это означает, что мы могли просто встроить эти проверки.
function padLeft(value: string, padding: string | number) < if (typeof padding === "number") < return Array(padding + 1).join(" ") + value; >if (typeof padding === "string") < return padding + value; >throw new Error(`Ожидал строку или число, а получил '$'.`); >
Эти typeof защитники типа распознаются в двух различных формах: typeof v === «typename» и typeof v !== «typename» , где «typename» — это «number» , «string» , «boolean» , или «symbol» . В то время как TypeScript не запрещает сравнение с другими строками или переключение двух сторон сравнения, язык не будет признавать эти формы как защитники типа.
Защитники типа instanceof
Если вы читали о защитнике типа typeof и знакомы с оператором instanceof в JavaScript, у Вас, вероятно, есть представление о чём этот раздел.
Защитники типа instanceof — это способ ограничения типов используя их функцию-конструктор. Например, давайте возьмём наш ранее введенный пример:
interface Padder < getPaddingString(): string >class SpaceRepeatingPadder implements Padder < constructor(private numSpaces: number) < >getPaddingString() < return Array(this.numSpaces + 1).join(" "); >> class StringPadder implements Padder < constructor(private value: string) < >getPaddingString() < return this.value; >> function getRandomPadder() < return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" "); >// Тип - 'SpaceRepeatingPadder | StringPadder' let padder: Padder = getRandomPadder(); if (padder instanceof SpaceRepeatingPadder) < padder; // type narrowed to 'SpaceRepeatingPadder' padder; // тип ограничен к 'SpaceRepeatingPadder' >if (padder instanceof StringPadder) < padder; // type narrowed to 'StringPadder' padder; // тип ограничен к 'StringPadder' >
Правая сторона instanceof должна быть функцией-конструктором, и TypeScript ограничит к:
- типу свойства prototype функции, если его тип не any
- объединению типов, возвращёнными констрами типов
Типы пересечения #
Типы пересечения тесно связаны с типами объединения, но они используются очень по-разному. Тип пересечения, Person & Serializable & Loggable , например, это Person и Serializable и Loggable . Это означает, что у объекта этого типа будут все элементы всех трёх типов. На практике Вы в большинстве случаев увидите типы перечисления, использованных для миксинов. Дальше простой пример миксина:
function extend(first: T, second: U): T & U < let result = <>; for (let id in first) < (result)[id] = (first)[id]; >for (let id in second) < if (!result.hasOwnProperty(id)) < (result)[id] = (second)[id]; >> return result; > class Person < constructor(public name: string) < >> interface Loggable < log(): void; >class ConsoleLogger implements Loggable < log() < // . >> var jim = extend(new Person("Jim"), new ConsoleLogger()); var n = jim.name; jim.log();
Псевдонимы(«Alias») типа #
Псевдонимы типа создают новое имя для типа. Псевдонимы типа иногда похожи на интерфейсы, но могут именовать примитивы, объединения, кортежи и любые другие типы, которые в противном случае Вам пришлось бы именовать вручную.
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name < if (typeof n === "string") < return n; >else < return n(); >>
Псевдоним на самом деле не создаёт новый тип, а создает новое имя, чтобы сослаться к тому типу. Псевдоним примитива не очень полезен, хотя его можно использовать как форму документации.
Так же, как интерфейсы, Псевдонимы типа могут быть универсальными — мы можем просто добавить параметры типа и использовать их с правой стороны объявления Псевдонима:
type Container = < value: T >;
У нас также может быть Псевдоним типа, имеющий ссылку на себя:
type Tree =
Вместе с типами пересечения мы можем сделать некоторые довольно галлюциногенные типы:
type LinkedList = T & < next: LinkedList >; interface Person < name: string; >var people: LinkedList; var s = people.name; var s = people.next.name; var s = people.next.next.name; var s = people.next.next.next.name;
Однако, у типа Псевдонима нет возможности появится с правой стороны объявления:
type Yikes = Array; // ошибка
Интерфейс vs. Тип Псевдоним
Как мы уже упоминали, тип Псевдоним может действовать как интерфейс; однако, существуют некоторые тонкие различия.
Одно важное различие в том, что от типа Псевдонима невозможно расшириться или реализоваться (к тому же, они не могут расширить/реализовать другие типы). Из-за того, что идеальное свойство программного обеспечения — быть открытым расширению, при возможности Вы всегда должны использовать интерфейс, чем тип Псевдоним.
С другой стороны, если Вы не можете выразить некоторую форму интерфейсом и Вам нужно использовать тип объединения или кортеж, тогда тип Псевдоним — тоже способ.
Тип строкового литерала #
Тип строкового литерала позволяет Вам указывать точное значение строки, которое она должна иметь. На практике тип строкового литерала хорошо комбинируется с типом объединения, защитниками типа и типом Псевдонима. Вы можете использовать эти функции вместе, чтобы получить поведение со строками как у перечисления.
type Easing = "ease-in" | "ease-out" | "ease-in-out"; class UIElement < animate(dx: number, dy: number, easing: Easing) < if (easing === "ease-in") < // . >else if (easing === "ease-out") < >else if (easing === "ease-in-out") < >else < // ошибка! null или undefined не должно передаваться. >> > let button = new UIElement(); button.animate(0, 0, "ease-in"); button.animate(0, 0, "uneasy"); // ошибка: "uneasy" не допускается
Вы можете передать любые из трёх разрешённых строк, но любая другая строка даст ошибку
Аргумент типа '"uneasy"' не может быть назначен параметром типа '"ease-in" | "ease-out" | "ease-in-out"'
Типы строкового литерала могут быть использованы тем же способом, чтобы различить перегрузки:
function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; // . больше перегрузок . function createElement(tagName: string): Element < // . код . >
Полиморфный тип this #
Полиморфный тип this представляет тип, являющийся подтипом содержащегося класса или интерфейса. Называется F-bounded полиморфизмом. И позволяет намного легче выразить иерархически плавающие интерфейсы, например. Возьмём простой калькулятор, возвращающий this после каждой операции:
class BasicCalculator < public constructor(protected value: number = 0) < >public currentValue(): number < return this.value; >public add(operand: number): this < this.value += operand; return this; >public multiply(operand: number): this < this.value *= operand; return this; >// . другие операции . > let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue();
Поскольку класс использует тип this , Вы можете расширить его (унаследоваться от него), и тогда новый класс сможет использовать старые методы без изменений.
class ScientificCalculator extends BasicCalculator < public constructor(value = 0) < super(value); >public sin() < this.value = Math.sin(this.value); return this; >// . другие операции . > let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue();
Без типа this класс ScientificCalculator не смог бы расширить (унаследовать) BasicCalculator и сохранить плавающий интерфейс. multiply возвратил бы BasicCalculator , который не имеет метода sin . Однако, с типом this метод multiply возвращает this , являющимся ScientificCalculator .
Поддержите перевод документации:
Поддерживатель | Github Репозиторий
Documentation generated by mdoc.
Введение #
Одним из основных принципов TypeScript является то, что проверка типов основывается на форме значений. Этот подход иногда называется «утиной типизацией» либо «структурным подтипированием». В TypeScript интерфейсы выполняют функцию именования типов, и являются мощным способом определения соглашений внутри кода, а также за пределами проекта.
Наш первый интерфейс #
Самый простой способ увидеть, как работают интерфейсы — начать с простого примера:
function printLabel(labelledObj: < label: string >) < console.log(labelledObj.label); >let myObj = ; printLabel(myObj);
Компилятор проверяет вызов printLabel . Эта функция принимает один параметр, который требует, чтобы у переданного объекта было свойство под именем label , которое имело бы строковый тип. Обратите внимание, что у нашего объекта есть и другие свойства, однако компилятор проверяет лишь то, что у него есть, по крайней мере, необходимые свойства, и их типы совпадают с требуемыми. В некоторых случаях, которые мы рассмотрим чуть позже, TypeScript ведет себя не так снисходительно.
Мы можем переписать этот пример, на этот раз используя интерфейс для того, чтобы отразить необходимость наличия свойства label строкового типа:
interface LabelledValue < label: string; >function printLabel(labelledObj: LabelledValue) < console.log(labelledObj.label); >let myObj = ; printLabel(myObj);
Интерфейс LabelledValue — это имя, которое теперь можно использовать, чтобы задать требование из предыдущего примера. Он по-прежнему отражает необходимость того, что объект должен иметь свойство строкового типа с именем label . Обратите внимание — совсем не обязательно явно указывать, что объект, который мы передаем в printLabel , реализует данный интерфейс, как пришлось бы делать в иных языках. В TypeScript имеет значение только форма объекта. Если объект, который передается в функцию, удовлетворяет перечисленным требованиям, то он считается подходящим.
Стоит отметить, что проверка типов не требует, чтобы свойства шли в определенном порядке: важно лишь, что необходимые свойства присутствуют и имеют подходящий тип.
Опциональные свойства #
Не все свойства интерфейса могут быть обязательными. Некоторые существуют только в определенных условиях, либо отсутствуют вообще. Такие необязательные (опциональные) свойства часто встречаются, к примеру, при передаче в функцию аргументов в виде объекта, в котором указаны всего несколько свойств.
Вот пример реализации такого приема:
interface SquareConfig < color?: string; width?: number; >function createSquare(config: SquareConfig): < let newSquare = ; if (config.color) < newSquare.color = config.color; >if (config.width) < newSquare.area = config.width * config.width; >return newSquare; > let mySquare = createSquare();
Интерфейсы с необязательными свойствами записываются подобно обычным, но каждое опциональное свойство помечается символом ? в конце имени.
Преимущество опциональных свойств заключается в том, что можно описать свойства, которые, возможно, будут присутствовать, и в то же время запретить использование тех свойств, которые не являются частью интерфейса. К примеру, если бы мы ошиблись при вводе имени color в createSquare , то получили бы сообщение об ошибке, информирующее об этом:
interface SquareConfig < color?: string; width?: number; >function createSquare(config: SquareConfig): < color: string; area: number >< let newSquare = ; if (config.color) < // Ошибка: Property 'collor' does not exist on type 'SquareConfig' newSquare.color = config.collor; >if (config.width) < newSquare.area = config.width * config.width; >return newSquare; > let mySquare = createSquare();
Свойства только для чтения #
Некоторые свойства должны быть изменяемыми только в момент создания объекта. Вы можете указать это, добавив readonly перед его именем:
interface Point
Создать объект Point можно присваиванием объектного литерала, но после присваивания изменить x и y будет больше нельзя.
let p1: Point = < x: 10, y: 20 >; p1.x = 5; // ошибка!
В TypeScript есть тип ReadonlyArray , который, по сути, является типом Array , из которого удалены все изменяющие его методы, так что можно быть уверенным, что такие массивы не будут изменяться после создания:
let a: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray = a; ro[0] = 12; // ошибка! ro.push(5); // ошибка! ro.length = 100; // ошибка! a = ro; // ошибка!
В последней строке примера можно видеть, что даже присваивание ReadonlyArray обычному массиву недопустимо. Впрочем, это ограничение все равно можно обойти, использовав приведение типов:
a = ro as number[];
readonly против const
Самый простой способ запомнить, когда нужно использовать readonly , а когда const — задать вопрос, нужна ли эта возможность для переменной либо для свойства объекта. С переменными используется const , а со свойствами — readonly .
Проверки на лишние свойства #
В нашем первом примере использования интерфейсов TypeScript позволил передать < size: number; label: string; >там, где ожидалось всего лишь < label: string; >. Также мы узнали о необязательных свойствах, и о том, как они могут быть полезны при передаче аргументов в функции.
Однако, бездумное сочетание двух этих возможностей позволило бы выстрелить себе в ногу так же, как и в JavaScript. К примеру, если взять последний пример с createSquare :
interface SquareConfig < color?: string; width?: number; >function createSquare(config: SquareConfig): < color: string; area: number > < // . >let mySquare = createSquare(< colour: "red", width: 100 >);
Обратите внимание, что аргумент, передаваемый в createSquare , записан как colour вместо color . В чистом JavaScript подобные вещи не выдают ошибок, но и не работают так, как хотел бы разработчик.
Можно сказать, что данная программа корректна с точки зрения типов, так как типы свойств width совместимы, color отсутствует, а наличие дополнительного свойства colour не имеет никакого значения.
Однако TypeScript делает предположение, что в этом куске кода есть ошибка. Литералы объектов обрабатываются им по-особенному, и проходят проверку на наличие лишних свойств. Эта проверка делается, когда литералы либо присваиваются другим переменным, либо передаются в качестве аргументов. Если в литерале есть какие-либо свойства, которых нет в целевом типе, то это будет считаться ошибкой.
// ошибка: 'colour' not expected in type 'SquareConfig' let mySquare = createSquare(< colour: "red", width: 100 >);
Обойти такую проверку очень легко. Самый простой способ — использовать приведение типов:
let mySquare = createSquare( < width: 100, opacity: 0.5 >as SquareConfig);
Если же вы уверены, что объект может иметь дополнительные свойства, которые будут использоваться каким-то особенным способом, то есть способ еще лучше — добавить строковый индекс. Если объекты SquareConfig могут иметь свойства color и width , а также любое количество иных свойств, то интерфейс можно описать следующим образом:
interface SquareConfig
Индексы мы обсудим чуть позже, а сейчас просто отметим, что в этом примере SquareConfig может иметь любое количество свойств, и, если это не color и не width , то их тип не имеет значения.
Последний способ обойти проверку на наличие избыточных свойств, который может показаться немножко неожиданным — присваивание объекта другой переменной. Так как squareOptions не будет проходить проверку на избыточные свойства, компилятор не выдаст ошибки.
let squareOptions = < colour: "red", width: 100 >; let mySquare = createSquare(squareOptions);
Не забывайте, что в простом коде, подобном приведенному выше, скорее всего, не нужно пытаться обойти эту проверку. Для более сложных объектных литералов, в которых есть методы, или которые имеют состояние, возможно, придется прибегнуть к помощи этой техники, однако большинство сообщений компилятора, связанных с проверкой на избыточные свойства, указывают на настоящие ошибки. Это значит, что когда вы сталкиваетесь с проблемами, которые порождает такая проверка (к примеру, при передаче в функцию объекта с аргументами), возможно, придется изменить объявления типов. В данном случае, если передача объекта, в котором могут быть одновременно свойства color и colour , приемлема, нужно исправить определение SquareConfig , чтобы отразить это.
Функциональные типы #
Интерфейсы могут описывать широкий диапазон «форм», которые принимают JavaScript-объекты. Кроме описания объектов со свойствами, интерфейсы могут описывать и типы функций.
Для того, чтобы описать функцию с помощью интерфейса, к нему добавляют сигнатуру вызова. Такая сигнатура выглядит как описание функции, в котором указаны только список аргументов и возвращаемый тип. Каждый параметр в списке должен иметь и имя, и тип.
interface SearchFunc
Будучи определенным, такой интерфейс может использоваться так же, как и другие интерфейсы. Сейчас мы покажем, как можно создать переменную функционального типа, и присвоить ей функцию.
let mySearch: SearchFunc; mySearch = function(source: string, subString: string) < let result = source.search(subString); if (result == -1) < return false; >else < return true; >>
Имена параметров не обязательно должны совпадать, чтобы функция прошла проверку на соответствие типов. Мы, к примеру, могли бы записать предыдущий пример вот так:
let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean < let result = src.search(sub); if (result == -1) < return false; >else < return true; >>
Параметры функций проверяются друг за другом, и типы параметров, находящихся на соответствующих позициях, сравниваются попарно. Если вы не хотите указывать типы для аргументов, то TypeScript сможет вывести типы из контекста, основываясь на том, что функция присваивается переменной, тип которой — SearchFunc . В следующем примере тип возвращаемого значения функции тоже выводится: это делается на основании значений, которые она возвращает ( false и true ). Если бы функция возвращала числа или строки, то компилятор во время проверки типов предупредил бы, что тип возвращаемого значения не совпадает с типом, указанным в интерфейсе SearchFunc .
let mySearch: SearchFunc; mySearch = function(src, sub) < let result = src.search(sub); if (result == -1) < return false; >else < return true; >>
Индексируемые типы #
Аналогично тому, как интерфейсы используются для описания функций, можно описать типы так, чтобы с ними можно было использовать оператор индекса — например, вот так a[10] или так ageMap[«daniel»] . Индексируемые типы имеют сигнатуру индекса, которая описывает типы, которые можно использовать для индексации объекта, а также типы значений, которые возвращает эта операция. Приведем пример:
interface StringArray < [index: number]: string; >let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];
Здесь у нас есть интерфейс StringArray , у которого есть сигнатура индекса. Эта сигнатура говорит о том, что когда StringArray индексируется числом, возвращается строка.
Существуют всего два вида поддерживаемых сигнатур индекса: со строками и с числами в качестве аргумента. Объект может поддерживать оба вида, но тип значения, который возвращается числовым индексом, должен быть подтипом того, который возвращается строковым индексом. Так сделано по той причине, что когда операция индекса применяется к объекту, JavaScript сначала преобразует переданное в качестве индекса число в строку. То есть использование индекса 100 (число) — то же самое, что использование «100» (строка), поэтому типы обоих индексов должны согласовываться.
class Animal < name: string; >class Dog extends Animal < breed: string; >// Ошибка: индексация строкой может вернуть объект Dog! interface NotOkay
Кроме того, что строковые индексы — мощный способ описания словарей, они диктуют требование того, чтобы типы всех свойств соответствовали типу, который возвращает операция индекса. Это происходит из-за того, что obj.property доступен и как obj[property] . В следующем примере тип name не совпадает с типом строкового индекса, и компилятор выдает ошибку:
interface NumberDictionary < [index: string]: number; length: number; // все хорошо, length — число name: string; // ошибка, the type of 'name' is not a subtype of the indexer >
Кроме того, сигнатуру индекса можно сделать доступной только для чтения, чтобы запретить присваивание индексам:
interface ReadonlyStringArray < readonly [index: number]: string; >let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // ошибка!
Установить myArray[2] нельзя, поскольку сигнатура индекса — только для чтения.
Типы классов #
Реализация интерфейса
В таких языках, как C# и Java интерфейсы наиболее часто используются для того, чтобы явно указать, что класс соответствует определенному соглашению. Это возможно и в TypeScript.
interface ClockInterface < currentTime: Date; >class Clock implements ClockInterface < currentTime: Date; constructor(h: number, m: number) < >>
Также в интерфейсе можно описать методы, которые реализованы внутри класса, как это сделано для setTime в следующем примере:
interface ClockInterface < currentTime: Date; setTime(d: Date); >class Clock implements ClockInterface < currentTime: Date; setTime(d: Date) < this.currentTime = d; >constructor(h: number, m: number) < >>
Интерфейсы описывают публичную часть класса, но не приватную. Это не дает возможности указывать с помощью интефейса, что класс должен использовать конкретные типы для своих приватных членов.
Разница между статической частью и экземпляром класса
Работая с классами и интерфейсами, полезно помнить, что класс имеет два типа: тип статической части и тип экземпляра. Вы могли столкнуться с ошибкой, если создавали интерфейс с конструктором, а потом пытались написать класс, который реализовывал бы его:
interface ClockConstructor < new (hour: number, minute: number); >class Clock implements ClockConstructor < currentTime: Date; constructor(h: number, m: number) < >>
Так происходит из-за того, что, когда класс реализует интерфейс, происходит проверка типа только его экземпляра. Конструктор же находится в статической части, и не включается в эту проверку.
Вместо такого подхода нужно работать напрямую со статической частью класса. В следующем примере мы определяем два интерфейса: ClockConstructor для конструктора, и ClockInterface для экземпляра класса. Затем, для удобства, мы определяем функцию-конструктор createClock , которая создает объекты того типа, который передается ей в качестве аргумента.
interface ClockConstructor < new (hour: number, minute: number): ClockInterface; >interface ClockInterface < tick(); >function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface < return new ctor(hour, minute); >class DigitalClock implements ClockInterface < constructor(h: number, m: number) < >tick() < console.log("beep beep"); >> class AnalogClock implements ClockInterface < constructor(h: number, m: number) < >tick() < console.log("tick tock"); >> let digital = createClock(DigitalClock, 12, 17); let analog = createClock(AnalogClock, 7, 32);
Так как первый параметр createClock имеет тип ClockConstructor , то в createClock(AnalogClock, 7, 32) происходит проверка на то, что AnalogClock имеет подходяющую сигнатуру конструктора.
Расширение интерфейсов #
Интерфейсы могут расширять друг друга, подобно классам. Это позволяет копировать члены одного интерфейса в другой, что дает больше гибкости при разделении интерфейсов на переиспользуемые компоненты.
interface Shape < color: string; >interface Square extends Shape < sideLength: number; >let square = <>; square.color = "blue"; square.sideLength = 10;
Интерфейс может расширять сразу несколько других интерфейсов, создавая их комбинацию:
interface Shape < color: string; >interface PenStroke < penWidth: number; >interface Square extends Shape, PenStroke < sideLength: number; >let square = <>; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
Гибридные типы #
Как говорилось ранее, интерфейсы способны описывать сложные типы, которые встречаются в «боевом» JavaScript. JavaScript — динамичный и гибкий язык, поэтому здесь можно внезапно столкнуться с объектами, которые работают как сочетание нескольких уже описанных типов.
Один из таких примеров — объект, который ведет себя и как функция, и как объект со свойствами:
interface Counter < (start: number): string; interval: number; reset(): void; >function getCounter(): Counter < let counter = function (start: number) < >; counter.interval = 123; counter.reset = function () < >; return counter; > let c = getCounter(); c(10); c.reset(); c.interval = 5.0;
При взаимодействии со сторонним кодом может понадобиться использовать подобные приемы, чтобы полноценно описать тип.
Интерфейсы, расширяющие классы #
Когда интерфейс расширяет класс, интерфейс наследует члены класса, но не их реализацию. Это аналогично тому, как если бы интерфейс описывал все члены класса, но не указывал их реализацию. Интерфейсы наследуют даже приватные и защищенные члены базового класса. Это означает, что если создать интерфейс, расширяющий класс с приватными или защищенными членами, то он может быть реализован только самим базовым классом либо его наследниками.
Это полезно в тех случаях, когда существует большая иерархия наследования, и нужно указать, что код работает только с определенными подклассами, у которых есть определенные свойства. Такие подклассы не обязаны иметь отношение друг к другу, кроме того, что они наследуются от одного и того же базового класса. К примеру:
class Control < private state: any; >interface SelectableControl extends Control < select(): void; >class Button extends Control < select() < >> class TextBox extends Control < select() < >> class Image extends Control < >class Location < select() < >>
В этом примере SelectableControl содержит все члены класса Control , включая приватное свойство state . Так как state — приватный член, реализовать интерфейс SelectableControl смогут только наследники Control . Так будет потому, что для совместимости приватных членов необходимо, чтобы они были объявлены в одном и том же базовом классе, а это возможно лишь для наследников Control .
Внутри кода Control можно получить доступ к приватному члену state через экземпляр SelectableControl . По сути, SelectableControl ведет себя так же, как Control , о котором известно, что у него есть метод select . Классы Button и TextBox — подтипы SelectableControl (так как оба унаследованы от Control и у них есть метод select ), однако Image и Location таковыми не являются.
Поддержите перевод документации:
Поддерживатель | Github Репозиторий
Documentation generated by mdoc.
Типы расширений в TypeScript
Создавая библиотеку TypeScript, вы неизбежно столкнетесь со сценарием, когда кто-то захочет ее расширить. В мире JavaScript это можно сделать с помощью следующего хака:
const yourLibrary = require('your-library'); const myExtension = require('./my-extension'); yourLibrary.yourObject.myExtension = myExtension
В TypeScript этот тип вещей обычно не одобряется, потому что система типов не разрешает «манкипатчинг». «Классический» обходной путь — использовать приведение к типу any , которое эффективно de-TypeScript-ifies TypeScript.
import < yourObject >from 'your-library'; import myExtension from 'my-extension'; (yourObject).myExtension = myExtension
Другая проблема с этим шаблоном заключается в том, что myExtension не может ссылаться на yourObject , что также требует приведения типов при каждом доступе.
const result = (yourObject).myExtension.myFunction();
На данный момент мы потеряли тип-безопасность myExtension . Таким образом, компилятор TypeScript больше не будет проверять, действительно ли myFunction существует в myExtension , не говоря уже о том, каков результат myFunction() . Слишком большая часть этого шаблона сделает ваш проект TypeScript непригодным для печати, и тогда JavaScript был бы лучшим вариантом.
Что делать?
Одно из решений — просто сделать запрос на перенос в исходную библиотеку, чтобы ваше расширение было включено в официальный пакет. Хотя в некоторых сценариях это может быть хорошей идеей, в большинстве случаев расширения слишком нишевые, сломанные или слишком большие, чтобы их можно было объединить в проект. Вдобавок ко всему, пул-реквесты часто требуют много времени, чтобы пройти проверку и включить в новый выпуск.
Другое решение, за которое выступает эта статья, — это создание библиотек с безопасными типами расширений. Я понимаю, что это не решит вашу проблему здесь и сейчас, но если вы являетесь автором библиотеки, это даст другим разработчикам простой способ расширить вашу работу, не затрагивая основной пакет. Если вы являетесь потребителем библиотеки, запрос на вытягивание, предоставляющий ей расширенные свойства, обычно намного проще, чем расширение библиотеки с помощью вашей конкретной функции.
Два типа расширений
Наиболее распространенными типами расширений, которые нужны разработчикам, являются расширения пересечения и расширения объединения. Расширения пересечения говорят: «Эй, ваши объекты потрясающие, но они были бы еще более потрясающими, если бы они использовали X». Расширения объединения говорят: «Эй, ваши объекты потрясающие, но вы упускаете несколько из них, которые мне понадобятся для сценария Y». Пересечения и объединения являются частью основного языка TypeScript — операторы пересечения & и объединения | являются основным способом создания составных типов. Я выступаю за использование этих операторов для расширения возможностей ваших библиотек.
Расширения пересечения
Расширения пересечения могут быть достигнуты с помощью универсального типа (назовем его U ), который передается вниз через ваши объекты и пересекается с примитивными объектами через оператор & .
Представим, что ваша библиотека содержит следующие два типа.
type Person = < name: string; address?: Address; friends?: Person[]; >type Address =
Расширения пересечений добавляют пересечение ко всем соответствующим типам.
type Person = < name: string; address?: Address; friends?: Person[]; > & U; type Address = < city: string; country: string; >& U;
Например, если мы хотим добавить необязательный параметр id ко всем типам, это становится простой операцией.
const me: Person> = < name: 'Mike', address: < id: 5, city: 'Helsinki', country: 'Finland' >, friends: [< name: 'Marie', id: 101 >] >
Более того, теперь у нас есть типизированный метод доступа id , поэтому следующая функция передаст компилятору TypeScript
const hasId = (p: Person>) => typeof p.id === 'number';
Расширения обьединения
Представим себе другой сценарий — мы создаем типы для объектов JSON.
type JSONPrimitive = number | boolean | string | null; type JSONValue = JSONPrimitive | JSONArray | JSONObject; type JSONObject = < [k: string]: JSONValue; >; interface JSONArray extends Array <>
Допустим, мы хотели бы, чтобы объекты JavaScript Date также принимались как JSON. Расширения обьединения, которые я представлю буквой T , дают нам простой способ сделать это.
type JSONPrimitive = number | boolean | string | null | T; type JSONValue = JSONPrimitive | JSONArray | JSONObject; type JSONObject = < [k: string]: JSONValue; >; interface JSONArray extends Array <>
Теперь мы можем размещать объекты Date по всему JSON, и компилятор TypeScript не будет жаловаться.
const jsonWithDates: JSONValue =
Проверка во время выполнения
Если вы используете валидатор типа среды выполнения, например io-ts , шаблоны очень похожи. Для пересечений мы можем использовать функцию intersection из io-ts .
import * as t from 'io-ts'; const PersonValidator = (u: t.TypeOf) = t.recursion( 'Person', t.intersection([ t.type(), t.partial(< address: AddressValidator(u), friends: t.array(PersonValidator(u)) >), u ])); const AddressValidator = (u: t.TypeOf) = t.intersection([ t.type(), u ]);
Тот же тип шаблона можно использовать для типов объединения — просто передайте валидатор t.union вместо t.intersection где это необходимо.
Покажи мне код!
Это стратегия, которую я использовал для построения json-schema-strictly-typed , которая создает типизированную версию схемы JSON, расширяемую как с помощью расширений пересечения, так и объединения. Таким образом, люди могут добавлять произвольные расширения к объектам в схеме (пересечение) и произвольные новые примитивы схемы (объединение).
С этого уровня универсальности легко экспортировать вспомогательные объекты для «базовых» случаев.. Базовый вариант расширения пересечения — это просто тип, от которого уже исходят все ваши объекты. В приведенном выше примере Person> и Address> будут такими, так как пересечение с <> — это no-op. Для типов объединения расширение по умолчанию может расширять библиотеку с помощью типа, который уже существует в объединении. Так, например, JSONSchemaValue не работает.
Я с нетерпением жду возможности увидеть, приживется ли этот шаблон и сможет ли сообщество придумать инструменты, которые помогут поддерживать и создавать библиотеки TypeScript с учетом расширяемости!
