Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Array containing all options of type value in Typescript

Tags:

typescript

Here is code forcing all options of type to present in keys of object (and not allowing other keys):

type Fruit = 'apple' | 'peach';
const objectWithAllFruitsAsKeys: {
  [ key in Fruit ]: any
} = { apple: '', peach: '' }

I am looking for typing formula, allowing close behaviour with values of array — to get array with all Fruit type's values presenting in it as values.

const arrayWithAllFruitsAsvalues: ??? = ['apple', 'peach'];

Do you guys have any ideas? Thank you in advance!

like image 664
wra Avatar asked Oct 22 '19 12:10

wra


People also ask

Can array have different data types in TypeScript?

In typescript, an array is a data type that can store multiple values of different data types sequentially.

How do you define an array of types in TypeScript?

An array in TypeScript can contain elements of different data types using a generic array type syntax, as shown below. let values: (string | number)[] = ['Apple', 2, 'Orange', 3, 4, 'Banana']; // or let values: Array<string | number> = ['Apple', 2, 'Orange', 3, 4, 'Banana'];

Should I use [] or array in TypeScript?

There is no difference at all. Type[] is the shorthand syntax for an array of Type . Array<Type> is the generic syntax. They are completely equivalent.

What type is {} in TypeScript?

The type object {} represents a 0-field object. The only legal value of this type is an empty object: {} . So the value { id: 1, name: "Foo" } is of type { id: number, name: string } , and the value {} (i.e. an empty object) is of type {} .


2 Answers

If you define the array first, here's a way to get the Fruit type

const arrayWithAllFruitsAsvalues = ['apple', 'peach'] as const;

type Fruit = typeof arrayWithAllFruitsAsvalues[number];  // "apple" | "peach"

const objectWithAllFruitsAsKeys: {
  [ key in Fruit ]: any
} = { apple: '', peach: '' }
like image 126
Istvan Szasz Avatar answered Dec 21 '22 08:12

Istvan Szasz


The type system isn't particularly geared toward guaranteeing that a given tuple has exactly the same values and number of elements as the member of a given union type. There are all kinds of ways to try to get this to happen.

I'll present one way: make an helper function that takes a union type (like Fruit) and returns an array builder that prompts the developer to add each individual member of the union, and at each step it removes the previously added members from the list of possible next elements, and finally only lets you build the array. This only really works for a union of literal types (or other unit types which have only one possible value each).

Here it is (and you could shove it in a library somewhere if it's too ugly to look at)

// TS4.0+
type Push<T extends readonly any[], V> = [...T, V];
type ReadonlyTuple<T> = Extract<Readonly<T>, readonly any[]>;

interface TupleBuilderAddable<U, T extends readonly any[]> {
    add<V extends U>(v: V): [U] extends [V] ?
        TupleBuilderBuildable<ReadonlyTuple<Push<T, V>>> :
        TupleBuilderAddable<Exclude<U, V>, ReadonlyTuple<Push<T, V>>>;
}
interface TupleBuilderBuildable<T extends readonly any[]> {
    build(): T;
}
function tupleBuilder<U>():
    [U] extends [never] ?
    TupleBuilderBuildable<[]> :
    TupleBuilderAddable<U, []> {
    const tuple: any[] = [];
    const ret = {
        add(v: any) {
            tuple.push(v);
            return ret;
        },
        build() {
            return tuple;
        }
    }
    return ret as any;
}

"Brief" explanation:

  • Push<T, V> takes a tuple type T and some type V and makes a new tuple with V appended to the end of T.
  • TupleBuilderBuildable<T> takes a tuple type T and represents an object with a build() method that returns a value of type T.
  • TupleBuilderAddable<U, T> takes a union type U and a tuple type T and represents an object with an add() method. This add() method accepts a value of type V that extends U. Conceptually it will remove V from U to get a new union type U', and then push V onto the T tuple to get a new tuple type T'. If U' is empty, add() returns TupleBuilderBuildable<T'>. Otherwise it returns TupleBuilderAddable<U', T'>. This is the important step in the type system: every time you call add(), it shifts one element from the union to the tuple.

At runtime, you just have a function that returns an object with both an add() and a build() method that pushes onto an array and returns an array, respectively. But at compile time only add() or build() is exposed depending on whether there's anything left in the union type that you haven't used.


Enough explanation; here's how you'd use it:

type Fruit = 'apple' | 'peach';

const t = tupleBuilder<Fruit>().add("apple").add("peach").build();
// const t: readonly ["apple", "peach"]

IntelliSense will only allow you to do certain things:

const builder0 = tupleBuilder<Fruit>();
// const builder0: TupleBuilderAddable<Fruit, []>
// builder0 only has an add() method
builder0.add("cherry"); // error! "cherry" is not assignable to Fruit.
const builder1 = builder0.add("peach"); // okay
// const builder1: TupleBuilderAddable<"apple", readonly ["peach"]>
// builder1 only has an add method
builder1.add("peach"); // error! "peach" is not assignable to "apple".
const builder2 = builder1.add("apple"); // okay
// const builder2: TupleBuilderBuildable<readonly ["peach", "apple"]>
// builder2 only has a build method
const u = builder2.build();
// const u: readonly ["peach", "apple"]

It forces you to add() "apple" and "cherry" in some order and then build(). Yes, it's verbose, but the compiler gives you hints the whole way through. You can't call build() before you've added all the elements, and you can't call add() when you have, and you can't add incorrect or repeated elements. And when you're all done you have a tuple type.


Not sure if this matches your use case, but I just wanted to show one possibility. Good luck!

Link to code

like image 28
jcalz Avatar answered Dec 21 '22 06:12

jcalz