Типы объектов
В JavaScript основным способом группировки и передачи данных являются объекты. В TypeScript мы представляем их с помощью типов объектов.
Как мы видели, они могут быть анонимными:
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
} или они могут быть именованными, используя интерфейс:
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
} или псевдоним типа:
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
} Во всех трёх примерах выше мы написали функции, которые принимают объекты, содержащие свойство name (которое должно быть string) и age (которое должно быть number).
Быстрый справочник
У нас есть шпаргалки как для type так и для interface, если вы хотите быстро ознакомиться с важными синтаксическими конструкциями.
Модификаторы свойств
Каждое свойство в типе объекта может указывать несколько вещей: тип, является ли свойство необязательным и может ли свойство быть изменено.
Необязательные свойства
Часто нам приходится иметь дело с объектами, которые могут иметь установленное свойство. В таких случаях мы можем пометить эти свойства как необязательные, добавив знак вопроса (?) в конец их имён.
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 }); В этом примере, оба xPos и yPos считаются необязательными. Мы можем выбрать, предоставить ли любой из них, поэтому каждый вызов выше paintShape является допустимым. Все необязательные свойства фактически говорят о том, что, если свойство есть установлено, оно должно иметь определённый тип.
Мы также можем считывать значения из этих свойств - но когда мы это делаем в соответствии с strictNullChecks, TypeScript сообщит нам, что они потенциально undefined.
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
let yPos = opts.yPos;
// ...
} В JavaScript, даже если свойство никогда не было установлено, мы всё равно можем к нему обратиться - оно просто вернёт значение undefined. Мы можем просто обработать undefined особенно, проверив его.
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
// ...
} Обратите внимание, что этот шаблон установки значений по умолчанию для неопределённых значений настолько распространён, что JavaScript имеет синтаксис для его поддержки.
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
console.log("y coordinate at", yPos);
// ...
} Здесь мы использовали деструктурирующий шаблон для параметра paintShape, и предоставили значения по умолчанию для xPos и yPos. Теперь xPos и yPos определённо присутствуют внутри тела paintShape, но являются необязательными для любых вызывающих сторон paintShape.
Обратите внимание, что в настоящее время нет возможности разместить аннотации типов внутри деструктурирующих шаблонов. Это потому, что следующий синтаксис уже имеет другое значение в JavaScript.
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
render(xPos);
} В деструктурирующем шаблоне объекта shape: Shape означает «получить свойство shape и переопределить его локально как переменную с именем Shape». Аналогично xPos: number создаёт переменную с именем number, значение которой основано на параметре xPos.
Используя модификаторы отображения, вы можете удалить optional атрибуты.
readonly свойства
Свойства также могут быть помечены как readonly для TypeScript. Хотя это не изменит поведения во время выполнения, свойство, помеченное как readonly , нельзя изменить при проверке типов.
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";
} Использование модификатора readonly не обязательно подразумевает, что значение полностью неизменяемо - другими словами, что его внутреннее содержимое не может быть изменено. Это просто означает, что само свойство не может быть переопределено.
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
name: "Victor the Evictor",
age: 42,
};
} Важно управлять ожиданиями того, что readonly подразумевает. Это полезно для сигнализации намерений во время разработки для TypeScript о том, как должен использоваться объект. TypeScript не учитывает, являются ли свойства двух типов readonly при проверке совместимости этих типов, поэтому свойства readonly могут также изменяться через алиасинг.
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43' Используя модификаторы отображения, вы можете удалить readonly атрибуты.
Подписи индексов
Иногда вы не знаете все имена свойств типа заранее, но вы знаете форму значений.
В таких случаях вы можете использовать подпись индекса для описания типов возможных значений, например:
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; Выше у нас есть интерфейс StringArray, который имеет подпись индекса. Эта подпись индекса указывает, что при индексировании объекта StringArray с помощью number, она вернёт string.
Для свойств подписи индекса разрешены только некоторые типы: string, number, symbol, шаблоны строк-шаблонов и объединения типов, состоящие только из них.
Возможна поддержка обоих типов индексаторов...
Возможна поддержка обоих типов индексаторов, но тип, возвращаемый числовым индексатором, должен быть подтипом типа, возвращаемого строковым индексатором. Это связано с тем, что при индексировании с помощью number, JavaScript фактически преобразует его в string перед индексированием объекта. Это означает, что индексирование с 100 (number) эквивалентно индексированию с "100" (string), поэтому эти два значения должны быть согласованы.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
} Хотя строковые подписи индексов являются мощным способом описания шаблона «словаря», они также обеспечивают, что все свойства соответствуют возвращаемому типу. Это потому, что строковый индекс заявляет, что obj.property также доступен как obj["property"] В следующем примере, тип name не соответствует типу строкового индекса, и система проверки типов выдаёт ошибку:
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
} Однако, свойства разных типов допустимы, если подпись индекса является объединением типов свойств:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
} Наконец, вы можете сделать подписи индексов readonly , чтобы предотвратить присвоение значений их индексам:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory"; Вы не можете установить myArray[2], потому что подпись индекса readonly.
Проверки на избыточные свойства
То, как и где объекту назначается тип, может повлиять на систему типов. Одним из ключевых примеров этого является проверка на избыточные свойства, которая проверяет объект более тщательно при его создании и присваивании типу объекта во время создания.
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
let mySquare = createSquare({ colour: "red", width: 100 }); Обратите внимание, что переданный аргумент createSquare имеет написание colour, а не color. В обычном JavaScript такая ситуация проходит безмолвно.
Можно утверждать, что эта программа имеет правильную типизацию, так как свойства width совместимы, свойства color не присутствуют, а дополнительное свойство colour несущественно.
Однако TypeScript полагает, что в этом коде, вероятно, есть ошибка. Объектные литералы получают специальное обращение и проходят проверку на избыточные свойства при присваивании их другим переменным или передаче в качестве аргументов. Если у объектного литерала есть какие-либо свойства, которых нет у «целевого типа», вы получите ошибку:
let mySquare = createSquare({ colour: "red", width: 100 }); Справиться с этими проверками на самом деле очень просто. Самый простой метод - использовать утверждение типа:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); Однако, лучшим подходом может быть добавление строковой подписи индекса, если вы уверены, что объект может иметь дополнительные свойства, которые используются каким-то специальным образом. Если SquareConfig может иметь свойства color и width с указанными выше типами, но также может иметь любое количество других свойств, тогда мы можем определить его следующим образом:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
} Мы обсудим подписи индексов немного позже, но здесь мы говорим о том, что SquareConfig может иметь любое количество свойств, и, пока они не являются color или width, их типы не важны.
Ещё один, возможно, несколько неожиданный способ обойти эти проверки - присвоить объект другой переменной: поскольку при присвоении squareOptions проверки на избыточные свойства не будут выполнены, компилятор не выдаст ошибку:
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions); Вышеуказанный обходной путь будет работать, если у squareOptions и SquareConfig есть общее свойство. В этом примере это свойство width Однако он потерпит неудачу, если у переменной нет общего объекта-свойства. Например:
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions); Помните, что для простого кода, подобного вышеприведённому, вы, вероятно, не должны пытаться «обходить» эти проверки. Для более сложных объектных литералов, имеющих методы и хранящих состояние, вам может потребоваться учитывать эти приёмы, но большая часть ошибок избыточной проверки - это, на самом деле, ошибки.
Это означает, что если вы сталкиваетесь с проблемами избыточной проверки свойств для чего-то вроде наборов опций, возможно, вам потребуется пересмотреть некоторые ваши определения типов. В этом случае, если передавать объект с обоими свойствами color или colour функции createSquare допустимо, вы должны исправить определение SquareConfig , отразив это.
Расширение типов
Довольно часто встречаются типы, которые могут быть более специфическими версиями других типов. Например, у нас может быть тип BasicAddress , описывающий поля, необходимые для отправки писем и посылок в США.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
} В некоторых ситуациях этого достаточно, но адреса часто имеют номер секции, если здание по адресу имеет несколько секций. Тогда мы можем описать AddressWithUnit.
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
} Это выполняет задачу, но недостаток здесь заключается в том, что нам пришлось повторить все остальные поля из BasicAddress , когда наши изменения были чисто добавочными. Вместо этого, мы можем расширить исходный тип BasicAddress и просто добавить новые поля, уникальные для AddressWithUnit.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
} Ключевое слово extends в interface позволяет эффективно копировать члены из других именованных типов и добавлять любые новые члены, которые нам нужны. Это может быть полезно для сокращения объёма шаблонного кода объявления типов, а также для обозначения того, что несколько различных объявлений одного и того же свойства могут быть взаимосвязаны. Например, AddressWithUnit не нужно было повторять свойство street, и поскольку street происходит от BasicAddress, читатель поймёт, что эти два типа связаны каким-то образом.
interface также могут расширяться от нескольких типов.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
}; Пересечения типов
interface позволяли нам создавать новые типы из других, расширяя их. TypeScript предоставляет ещё один конструкт, называемый пересечениями типов, который используется главным образом для объединения существующих типов объектов.
Тип пересечения определяется с помощью оператора &.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle; Здесь мы объединили Colorful и Circle , чтобы получить новый тип, который содержит все члены Colorful и Circle.
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 }); Интерфейсы против пересечений
Мы только что рассмотрели два похожих, но на самом деле немного разных способа объединения типов. С помощью интерфейсов мы могли использовать клаузу extends для расширения от других типов, и мы могли сделать что-то подобное с пересечениями и назвать результат псевдонимом типа. Основное различие между ними заключается в том, как обрабатываются конфликты, и это различие обычно является одной из основных причин, по которой вы бы выбрали один из них (интерфейс или псевдоним типа пересечения).
Обобщённые типы объектов
Представьте себе тип Box , который может содержать любое значение — string , number , Giraffe и так далее.
interface Box {
contents: any;
} Сейчас свойство contents имеет тип any, что работает, но может привести к ошибкам в дальнейшем.
Вместо этого мы могли бы использовать unknown, но это означало бы, что в случаях, когда мы уже знаем тип contents, нам нужно было бы проводить предосторожные проверки или использовать подверженные ошибкам утверждения типов.
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// or we could use a type assertion
console.log((x.contents as string).toLowerCase()); Один из способов обеспечения безопасности типов заключался бы в создании различных Box типов для каждого типа contents.
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
} Но это означает, что нам придётся создавать разные функции или перегрузки функций для работы с этими типами.
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
} Это много шаблонного кода. Более того, нам, возможно, в будущем потребуется ввести новые типы и перегрузки. Это неудобно, так как наши типы коробок и перегрузки фактически одинаковы.
Вместо этого мы можем создать обобщённый Box тип, который объявляет параметр типа.
interface Box<Type> {
contents: Type;
} Это можно прочитать как «% Box Type — это нечто, чьи члены имеют тип Type». В дальнейшем, когда мы будем ссылаться на Box, нам нужно будет указать аргумент типа вместо Type.
let box: Box<string>;
Представьте Box как шаблон для реального типа, где Type — это заполнитель, который будет заменён другим типом. Когда TypeScript видит Box<string>, он заменит каждое вхождение Type в Box<Type> на string, и в конечном итоге будет работать с чем-то вроде { contents: string }. Другими словами, Box<string> и наши предыдущие StringBox работают идентично.
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
let boxB: StringBox = { contents: "world" };
boxB.contents; Box является многоразовым, поскольку Type можно заменить чем угодно. Это означает, что когда нам нужна коробка для нового типа, нам вообще не нужно объявлять новый Box тип (хотя мы могли бы, если захотим).
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>; Это также означает, что мы можем полностью избежать перегрузок, используя вместо этого обобщённые функции обобщённые функции.
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
} Стоит отметить, что псевдонимы типов также могут быть обобщёнными. Мы могли бы определить наш новый Box<Type> интерфейс, который был:
interface Box<Type> {
contents: Type;
} используя вместо него псевдоним типа:
type Box<Type> = {
contents: Type;
}; Поскольку псевдонимы типов, в отличие от интерфейсов, могут описывать не только типы объектов, мы можем использовать их и для написания других типов вспомогательных обобщённых типов.
type OrNull<Type> = Type | null; type OneOrMany<Type> = Type | Type[]; type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>; type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
Мы вернёмся к псевдонимам типов чуть позже.
Тип Array
Обобщённые типы объектов часто являются контейнерными типами, которые работают независимо от типа содержащихся в них элементов. Это идеально подходит для структур данных, чтобы они работали таким образом, чтобы их можно было повторно использовать для разных типов данных.
Оказывается, мы работали с типом, подобным этому на протяжении всего руководства: тип Array . Всякий раз, когда мы записываем типы, такие как number[] или string[], это всего лишь сокращение для Array<number> и Array<string>.
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world")); Как и тип Box выше, сам тип Array является обобщённым типом.
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
} Современный JavaScript также предоставляет другие структуры данных, которые являются обобщёнными, такие как Map<K, V>, Set<T> и Promise<T>. Всё это означает, что из-за того, как Map, Set и Promise ведут себя, они могут работать с любыми наборами типов.
Тип ReadonlyArray
Тип ReadonlyArray — это специальный тип, описывающий массивы, которые не должны изменяться.
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
} Подобно модификатору readonly для свойств, он в основном служит инструментом для обозначения намерений. Когда мы видим функцию, возвращающую ReadonlyArray , это означает, что нам не следует изменять содержимое вообще; когда мы видим функцию, которая потребляет ReadonlyArray , это означает, что мы можем передать в эту функцию любой массив, не беспокоясь о том, что он изменит его содержимое.
В отличие от Array, нет конструктора ReadonlyArray , который мы могли бы использовать.
new ReadonlyArray("red", "green", "blue"); Вместо этого мы можем назначить обычные Array массивы ReadonlyArray массивам.
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
Так же как TypeScript предоставляет сокращённую запись для Array<Type> с помощью Type[], он также предоставляет сокращённую запись для ReadonlyArray<Type> с помощью readonly Type[].
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
} Ещё одно замечание: в отличие от модификатора свойства readonly, присваивание не является двунаправленным между обычными Array массивами и ReadonlyArray массивами.
let x: readonly string[] = []; let y: string[] = []; x = y; y = x;
Кортежи типов
Тип кортежа — это ещё один вид Array типа, который знает точно, сколько элементов он содержит и какие типы он содержит в конкретных позициях.
type StringNumberPair = [string, number];
Здесь StringNumberPair — это тип кортежа string и number. Как и ReadonlyArray, у него нет представления во время выполнения, но это существенно для TypeScript. Для системы типов StringNumberPair описывает массивы, в которых элемент с индексом 0 содержит string, а элемент с индексом 1 содержит number.
function doSomething(pair: [string, number]) {
const a = pair[0];
const b = pair[1];
// ...
}
doSomething(["hello", 42]); Если мы попытаемся обратиться к индексу, выходящему за пределы количества элементов, мы получим ошибку.
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];
} Мы также можем распаковать кортежи с помощью распаковки массивов JavaScript.
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString);
console.log(hash);
} Типы кортежей полезны в API, сильно опирающихся на соглашения, где значение каждого элемента «очевидно». Это даёт нам гибкость в именовании переменных при распаковке. В приведённом выше примере мы можем назвать элементы
0и1как угодно.Однако, поскольку не все пользователи имеют одинаковое представление о том, что является очевидным, возможно, стоит пересмотреть, не лучше ли использовать объекты с описательными именами свойств для вашего API.
Помимо этих проверок длины, простые типы кортежей, такие как эти, эквивалентны типам, которые являются версиями Array , объявляющим свойства для конкретных индексов и объявляющим length с числовым литеральным типом.
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
} Ещё один момент, который вас может заинтересовать: кортежи могут иметь необязательные свойства, записывая вопросительный знак (? ) после типа элемента. Необязательные элементы кортежей могут появляться только в конце и также влияют на тип length.
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
console.log(`Provided coordinates had ${coord.length} dimensions`);
} Кортежи также могут иметь остаточные элементы, которые должны быть массивом/типом кортежа.
type StringNumberBooleans = [string, number, ...boolean[]]; type StringBooleansNumber = [string, ...boolean[], number]; type BooleansStringNumber = [...boolean[], string, number];
-
StringNumberBooleansописывает кортеж, в котором первые два элемента —stringиnumberсоответственно, но после них может быть любое количествоboolean. -
StringBooleansNumberописывает кортеж, в котором первый элемент —string, а затем любое количествоbooleanи заканчиваетсяnumber. -
BooleansStringNumberописывает кортеж, в котором начальные элементы — любое количествоboolean, затемstringиnumber.
Кортеж с остаточным элементом не имеет фиксированной «длины» — у него есть только набор известных элементов в разных позициях.
const a: StringNumberBooleans = ["hello", 1]; const b: StringNumberBooleans = ["beautiful", 2, true]; const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
В чём польза необязательных и остаточных элементов? Она позволяет TypeScript сопоставлять кортежи со списками параметров. Кортежи типов могут использоваться в остаточных параметрах и аргументах, так что следующее:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
} в основном эквивалентно следующему:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
} Это полезно, когда вы хотите использовать переменное количество аргументов с остаточным параметром, и вам нужен минимальный набор элементов, но вы не хотите вводить промежуточные переменные.
readonly Типы кортежей
Окончательное примечание о типах кортежей: типы кортежей имеют readonly варианты и могут быть заданы с помощью модификатора readonly перед ними — точно так же, как с сокращённой записью массивов.
function doSomething(pair: readonly [string, number]) {
// ...
} Как вы можете ожидать, запись в любое свойство кортежа readonly не допускается в TypeScript.
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
} Кортежи, как правило, создаются и остаются неизменными в большинстве кода, поэтому аннотирование типов как readonly кортежи, когда это возможно, является хорошим значением по умолчанию. Это также важно, учитывая, что литералы массивов с const утверждениями будут выведены с readonly типами кортежей.
let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point); Здесь, distanceFromOrigin никогда не изменяет свои элементы, но ожидает изменяемый кортеж. Поскольку тип point был выведен как readonly [3, 4], он не будет совместим с [number, number], поскольку этот тип не может гарантировать, что элементы point не будут изменены.
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/2/objects.html