Record & Tuple proposal and TypeScript 4.0

Относительно недавно (в мае) Робин Рикард и Рик Баттон сделали предложение «Record & Tuple», которое добавляет два новых примитивных объектов в JavaScript:

  • кортежи (tuples) — неизменяемая и сравниваемая по значению версия массивов
  • записи (records) — неизменяемая и сравниваемая по значению версия простых объектов

Основная цель новых типов - это возможность сравнивать эти объекты по значения. Сейчас, как вы знаете, все объекты не равны друг другу.

А в TypeScript 4.0 добавляют еще и labels to tuple elements.

Но если вам нужны кортежи уже сейчас, как структуры данных, то их можно, в принципе эмулировать. Еще два года назад я писал подробно про кортежи в JS и TypeScript.

Что такое кортеж?

Коротко напомню, что кортежи несут еще один смысл:

Кортеж (tuple) — упорядоченный набор фиксированной длины.

Сейчас во многих языках программирования существует такая конструкция. Где-то кортежи встроены в язык, а где-то реализуются средствами библиотек. Кортежи есть, например, в языках Erlang, F#, Groovy, Haskell, Lisp, C#, D, Python, Ruby, Go, Rust, Swift и многих других…

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

На самом деле вы уже используете кортежи в JavaScript неявно и даже не подозреваете об этом. Кортежи неявно используются во всех языках программирования, даже в Си и Ассемблере.

Список аргументов функции или список инициализации массива является неявным кортежем

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

Список инициализации массива — это тоже кортеж. Даже обычный блок кода — это тоже кортеж. Только элементами его являются не значения или объекты, а синтаксические конструкции.

Явно реализуем кортежи в JS/ES2020

Так как в JS нет синтаксической конструкции для объявления кортежа, мы создадим функцию tuple (прям как в Python):

const tuple = (...args) => Object.freeze(args)

// Usage:
const tup = tuple ( 1, 2, 3, 4 )

tup[0] = 13 // ничего не произойдет

console.log( tup ) // [1,2,3,4]

//

Мы получили неизменяемый список фиксированной длины.

Так же можно реализовать разные частные случаи кортежей, например - пара.

Частный случай кортежа  —  пара

Как понятно из названия пара может содержать только 2 значения, в то время как кортеж — любое количество. Реализация пары на JavaScript:


const pair = (...args) => Object.freeze(args.slice(0,2))
// или так, ведь мы знаем что у нас могут быть только 2 аргумента
const pair = (x, y) => Object.freeze([x,y])

// Пример использования:

const par = pair ( 1, 2, 3 ) // 3 не добавится

par[0] = 3 // нельзя изменить
par[4] = 4 // нельзя добавить

console.log(par) // [1,2]

//

Кортежи в TypeScript

TypeScript уже давно позволяет типизировать структуры данных и дает нам возможность описать кортеж, содержащий разные типы. Допустим мы хотим кортеж, который содержит сразу два типа:


const myTuple: [string, number] = ['foo', 123]

myTuple[0] = 123

//
// Type '123' is not assignable to type 'string'.
//

Так же кортеж можно описать через интерфейс, расширив базовый тип Array:


interface MyTuple<T,U> extends Array<T|U> {
    0: T;
    1: U;
}

const mytuple: MyTuple<boolean, number> = [true, 123];

//

Таким образом можно создавать списки с фиксированными типами. Но если вспомнить что такое кортеж — это упорядоченный набор фиксированной длины. А у нас получились объекты, в которые можно добавить новые элементы.

Упорядоченный набор фиксированной длины

Мы можем указать длину нашего кортежа и TS будет проверять ее:



interface MyTuple extends Array<number | string> {
    0: number;
    1: string;
    length: 2; // это литеральный тип '2', это не значение!
}


const mytuple: MyTuple = [123, 'abc', 'foo']

//
// Type '[number, string, string]'
// is not assignable to type 'Tuple'. Types of property
//

Как видите, TS говорит нам, что мы не можем объявить кортеж с тремя элементами, так как мы заявили всего 2. Но, как помните из предыдущих упоминаний, все это артефакты. Поэтому для рантайма нам нужно добавить заморозку объекта.

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



interface MyTuple extends ReadonlyArray<string|number> {
    0: string;
    1: number;
    length: 2;
}


const tup: MyTuple = <MyTuple>Object.freeze(['foo', 123])
// или
const tup: MyTuple = Object.freeze(['foo', 123]) as MyTuple;
// или
const tup: MyTuple = ['foo', 123]; Object.freeze(tup);

Описание кортежа таким способом дает следующие преимущества:

  • нельзя создать изначально кортеж больше заданной длины



const tup: MyTuple = <MyTuple>Object.freeze(['foo', 123, 456])


/*
Type '[string, number, number]'
is not assignable to type 'MyTuple'.
Types of property 'length' are incompatible.
Type '3' is not assignable to type '2'.
*/
  • типы элементов упорядочены и их нельзя менять местами

tup[0] = 123

// Type '123' is not assignable to type 'string'
  • нельзя модифицировать существующие элементы
  • нельзя добавлять новые элементы

tup[3] = 456

// Index signature in type 'MyTuple' only permits reading

Кортежи в TypeScript 4.0

Собственно что предлагается в новой версии TS в области кортежей - это добавление нового синтаксиса:


type MyTupple = [length: number, count: number];


function createMyTupple(/* ... */): [length: number, count: number] {
  /* ... */
}

Про новый TS 4.0 подробнее по ссылке:

Announcing TypeScript 4.0 Beta