Tìm kiếm trang web

typescript - Liên hợp chuỗi thành mảng chuỗi


Tôi có một kiểu hợp chuỗi như sau:

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

Tôi muốn một cách an toàn về kiểu để lấy tất cả các giá trị có thể được sử dụng trong hợp nhất chuỗi này. Nhưng vì các interface chủ yếu là một cấu trúc thời gian thiết kế, nên điều tốt nhất tôi có thể làm là như sau:

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;
}

Điều này hoạt động ổn, hàm đảm bảo tôi luôn truyền một đối tượng mà mỗi khóa là một phần tử trong hợp nhất chuỗi và mọi phần tử đều được bao gồm, và trả về một mảng chuỗi của tất cả các phần tử. Vì vậy, nếu hợp nhất chuỗi thay đổi, lời gọi đến hàm này sẽ báo lỗi tại thời điểm biên dịch nếu không được cập nhật.

Tuy nhiên, vấn đề là chữ ký kiểu cho hằng số ALL_SUITS là ('hearts' | 'diamonds' | 'spades' | 'clubs')[]. Nói cách khác, TypeScript nghĩ rằng đó là một mảng chứa không hoặc nhiều giá trị trong số những giá trị này có thể bị trùng lặp, thay vì một mảng chứa tất cả các giá trị chỉ một lần, ví dụ ['hearts', 'diamonds', 'spades', 'clubs'].

Điều tôi thực sự muốn là một cách cho hàm getAllStringUnionValues tổng quát của tôi để chỉ định rằng nó trả về ['hearts', 'diamonds', 'spades', 'clubs'].

Làm thế nào tôi có thể đạt được điều này một cách tổng quát trong khi vẫn giữ cho nó càng DRY càng tốt?

Câu trả lời #1

Trả lời cho TypeScript 3.4 trở lên

Thực sự không thể chuyển một union thành tuple trong TypeScript, ít nhất là không theo cách mà hoạt động ổn định. Union được thiết kế là không có thứ tự, và tuple vốn dĩ là có thứ tự, vì vậy ngay cả khi bạn cố gắng làm điều đó, các tuple kết quả có thể hoạt động theo những cách không mong muốn. Xem câu trả lời này để biết một phương pháp thực sự tạo ra tuple từ union, nhưng với rất nhiều cảnh báo về mức độ dễ bị lỗi của nó. Ngoài ra, xem microsoft/TypeScript#13298, một yêu cầu tính năng bị từ chối cho việc chuyển union thành tuple, để thảo luận và có câu trả lời chính thức về lý do tại sao điều này không được hỗ trợ.

Tuy nhiên, tùy thuộc vào trường hợp sử dụng của bạn, bạn có thể đảo ngược vấn đề: chỉ định rõ ràng kiểu tuple và suy ra union từ nó. Điều này khá trực tiếp.

Bắt đầu từ TypeScript 3.4, bạn có thể sử dụng một const assertion để nói với trình biên dịch suy luận kiểu của một tuple các literal như một tuple của các literal, thay vì, ví dụ, string[]. Nó có xu hướng suy luận kiểu hẹp nhất có thể cho một giá trị, bao gồm việc làm cho mọi thứ trở nên readonly. Vì vậy bạn có thể làm như sau:

const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const;
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number];  // "hearts" | "diamonds" | "spades" | "clubs"

Liên kết Playground đến mã


Trả lời cho TypeScript 3.0 đến 3.3

Có vẻ như, bắt đầu từ TypeScript 3.0, TypeScript sẽ có thể tự động suy diễn các kiểu tuple. Khi điều đó được phát hành, hàm tuple() mà bạn cần có thể được viết một cách ngắn gọn như sau:

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

Và sau đó bạn có thể sử dụng nó như thế này:

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

Trả lời cho TypeScript trước 3.0

Kể từ khi tôi đăng câu trả lời này, tôi đã tìm ra một cách để suy ra kiểu tuple nếu bạn sẵn sàng thêm một hàm vào thư viện của mình. Hãy xem hàm tuple() trong file tuple.ts.

Sử dụng nó, bạn có thể viết những điều sau mà không lặp lại chính mình:

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

Câu trả lời gốc

Cách trực tiếp nhất để đạt được những gì bạn muốn là chỉ định kiểu tuple một cách rõ ràng và suy ra tập hợp hợp nhất từ nó, thay vì cố gắng ép TypeScript làm ngược lại, vì TypeScript không biết cách làm điều đó. Ví dụ:

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

Lưu ý rằng bạn vẫn đang viết ra các giá trị literal hai lần, một lần dưới dạng kiểu trong SuitTuple và một lần dưới dạng giá trị trong ALL_SUITS; bạn sẽ thấy không có cách nào hay để tránh việc lặp lại này, vì hiện tại TypeScript không thể được yêu cầu suy luận các tuple, và nó sẽ không bao giờ tạo ra mảng runtime từ kiểu tuple.

Ưu điểm ở đây là bạn không cần phải liệt kê key của một đối tượng giả tại thời điểm chạy. Tất nhiên, bạn vẫn có thể xây dựng các kiểu với các chất bài làm key nếu bạn vẫn cần chúng:

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

Câu trả lời #2

Cập nhật cho TypeScript 3.4:

Sẽ có một cú pháp ngắn gọn hơn đi kèm với TypeScript 3.4 gọi là "const contexts". Nó đã được hợp nhất vào nhánh chính và dự kiến sẽ sớm có mặt như đã thấy trong PR này.

Tính năng này sẽ cho phép tạo một kiểu tuple/mảng bất biến (hằng số) bằng cách sử dụng các từ khóa as const hoặc <const>. Vì mảng này không thể bị sửa đổi, TypeScript có thể an toàn giả định một kiểu literal hẹp ['a', 'b'] thay vì kiểu rộng hơn ('a' | 'b')[] hoặc thậm chí là string[] và chúng ta có thể bỏ qua việc gọi hàm tuple().

Để trả lời câu hỏi của bạn

Tuy nhiên vấn đề là chữ ký kiểu cho hằng số ALL_SUITS là ('hearts' | 'diamonds' | 'spades' | 'clubs')[]. (... nó nên là) ['hearts', 'diamonds', 'spades', 'clubs']

Với cú pháp mới, chúng ta có thể đạt được chính xác điều đó:

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']

Với mảng bất biến này, chúng ta có thể dễ dàng tạo kiểu hợp nhất mong muốn:

type Suits = typeof ALL_SUITS[number]  

Câu trả lời #3

Dễ dàng và ngay trong trái tim.

Chuyển đổi String Union thành mảng string - quyết định đúng đắn!

type ValueOf<T> = T[keyof T];

type NonEmptyArray<T> = [T, ...T[]]

type MustInclude<T, U extends T[]> = [T] extends [ValueOf<U>] ? U : never;

function stringUnionToArray<T>() {
  return <U extends NonEmptyArray<T>>(...elements: MustInclude<T, U>) => elements;
}


/* USAGE */
type Variants = "error" | "success" | "info";

// This is what You want!! :)
let stringArray = stringUnionToArray<Variants>()("error", "success", "info");

Bài viết liên quan