TypeScript Строковый союз для строкового массива

У меня есть тип объединения строк, например:

type Suit = 'hearts' | 'diamonds' | 'spades' | 'clubs';

Мне нужен безопасный тип, чтобы получить все возможные значения, которые можно использовать в этом объединении строк. Но поскольку интерфейсы в основном являются конструкцией-временем, я могу это сделать лучше всего:

export const ALL_SUITS = getAllStringUnionValues<Suit>({
    hearts: 0,
    diamonds: 0,
    spades: 0,
    clubs: 0
});

export function getAllStringUnionValues<TStringUnion extends string>(valuesAsKeys: { [K in TStringUnion]: 0 }): TStringUnion[] {
    const result = Object.getOwnPropertyNames(valuesAsKeys);
    return result as any;
}

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

Однако проблема, сигнатура типа для константы ALL_SUITS равна ('hearts' | 'diamonds' | 'spades' | 'clubs')[]. Другими словами, TypeScript считает, что это массив, содержащий не более или менее этих значений, возможно, с дубликатами, а не массив, содержащий все значения только один раз, например. ['hearts', 'diamonds', 'spades', 'clubs'].

Мне бы очень хотелось, чтобы моя универсальная функция getAllStringUnionValues указывала, что она возвращает ['hearts', 'diamonds', 'spades', 'clubs'].

Как я могу достичь этого в общем, будучи DRY насколько возможно?

Ответ 1

Обновление июль 2018

Похоже, начиная с TypeScript 3.0, для TypeScript будет возможно автоматически выводить типы кортежей. После того, как это будет выпущено, tuple() вам функцию tuple() можно кратко записать так:

export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

И тогда вы можете использовать это так:

const ALL_SUITS = tuple('hearts', 'diamonds', 'spades', 'clubs');
type SuitTuple = typeof ALL_SUITS;
type Suit = SuitTuple[number];  // union type

Обновление август 2017

Так как я опубликовал этот ответ, я нашел способ определить типы кортежей, если вы хотите добавить функцию в свою библиотеку. Проверьте функцию tuple() в tuple.ts.

export type Lit = string | number | boolean | undefined | null | void | {};

// infers a tuple type for up to twelve values (add more here if you need them)
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit, K extends Lit, L extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L): [A, B, C, D, E, F, G, H, I, J, K, L];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit, K extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K): [A, B, C, D, E, F, G, H, I, J, K];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit, J extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J): [A, B, C, D, E, F, G, H, I, J];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit, I extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I): [A, B, C, D, E, F, G, H, I];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit, H extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): [A, B, C, D, E, F, G, H];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit, G extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F, g: G): [A, B, C, D, E, F, G];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit, F extends Lit>(a: A, b: B, c: C, d: D, e: E, f: F): [A, B, C, D, E, F];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit, E extends Lit>(a: A, b: B, c: C, d: D, e: E): [A, B, C, D, E];
export function tuple<A extends Lit, B extends Lit, C extends Lit, D extends Lit>(a: A, b: B, c: C, d: D): [A, B, C, D];
export function tuple<A extends Lit, B extends Lit, C extends Lit>(a: A, b: B, c: C): [A, B, C];
export function tuple<A extends Lit, B extends Lit>(a: A, b: B): [A, B];
export function tuple<A extends Lit>(a: A): [A];
export function tuple(...args: any[]): any[] {
  return args;
}

Используя его, вы можете написать следующее и не повторять себя:

const ALL_SUITS = tuple('hearts', 'diamonds', 'spades', 'clubs');
type SuitTuple = typeof ALL_SUITS;
type Suit = SuitTuple[number];  // union type

Как вы думаете?


Оригинальный ответ

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

type SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs'];
const ALL_SUITS: SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs']; // extra/missing would warn you
type Suit = SuitTuple[number];  // union type

Обратите внимание, что вы все еще пишете литералы дважды, один раз как типы в SuitTuple и один раз как значения в ALL_SUITS; вы не найдете там отличного способа избежать повторения себя таким образом, поскольку в настоящее время TypeScript нельзя сказать, чтобы выводить кортежи, и он никогда не сгенерирует массив времени выполнения из типа кортежа.

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

const symbols: {[K in Suit]: string} = {
  hearts: '♥', 
  diamonds: '♦', 
  spades: '♠', 
  clubs: '♣'
}

Надеюсь, это поможет.

Ответ 2

Обновление для TypeScript 3.4:

В TypeScript 3.4 будет более краткий синтаксис, называемый "const context". Он уже объединен с мастером и должен быть доступен в ближайшее время, как показано в этом PR.

Эта функция позволит создать неизменяемый (постоянный) тип/массив кортежей, используя ключевые слова as const или <const>. Поскольку этот массив нельзя изменить, TypeScript может смело принимать узкий литеральный тип ['a', 'b'] вместо более широкого ('a' | 'b')[] или даже string[] типа string[] и мы можем пропустить вызов функции tuple().

Чтобы обратиться к вашему вопросу

Однако проблема заключается в том, что сигнатура типа для константы ALL_SUITS ("сердца" | "бриллианты" | "пики" | "клубы") []. (... так и должно быть) ["сердца", "бриллианты", "пики", "клубы"]

С новым синтаксисом мы можем достичь именно этого:

const ALL_SUITS = <const> ['hearts', 'diamonds', 'spades', 'clubs'];  
// or 
const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const;

// type of ALL_SUITS is infererd to ['hearts', 'diamonds', 'spades', 'clubs']

С помощью этого неизменяемого массива мы можем легко создать желаемый тип объединения:

type Suits = typeof ALL_SUITS[number]