4

Is there a way to require array elements with typescript so that I can do have

type E = keyof T; // Number of properties in T is unknown

let T be defined as in this example:

interface T{
   el1:number,
   el2:number,
   el3:number
}

The resulting typeguard should check that all and only the properties of T should be exposed in the resulting array. For example using the example T from above:

[{"arg":"el1"},{"arg":"el2"},{"arg":"el3"}]  //only correct option
[{"arg":"el1"},{"arg":"el2"}]  // should fail
[{"arg":"el1"},{"arg":"el2"},{"arg":"el2"},{"arg":"el3"}]  // should fail
[{"arg":"el1"},{"arg":"el2"},{"arg":"el8"}]  // should fail

Currenlty I do use

type ITestElements = {
    fn: E
}[];

which only covers the second example as positive too.

LeDon
  • 529
  • 7
  • 21
  • What's the specific problem you're trying to solve with this? If you want a specific number of elements, you probably want a [tuple type](http://www.typescriptlang.org/docs/handbook/basic-types.html#tuple). – jonrsharpe Jan 10 '20 at 11:13
  • 1
    I do not know the number of elements of E before. Every array element should be unique and corresponding to the Elements of E. – LeDon Jan 10 '20 at 11:18
  • How do you not know that? It's in your codebase, no? – jonrsharpe Jan 10 '20 at 11:18
  • It's used inside an outside exposed method. E is populated by type E= keyof T; which itself is an unknown Object. – LeDon Jan 10 '20 at 11:24
  • Could you add this context to the question? Again, what's the problem you're trying to solve? – jonrsharpe Jan 10 '20 at 11:31
  • I want the typeguard to check that all properties of T should be included inside the array which is defined due to the typeguard – LeDon Jan 10 '20 at 11:38
  • Yes, but that's not *context*, and it's not *in the question*. *Why* do you want this? What's the *problem*? – jonrsharpe Jan 10 '20 at 11:39
  • To make sure that all properties of T are mapped correctly to the array and let the compiler throw an error if this requirement is not fullfilled. – LeDon Jan 10 '20 at 11:45
  • I feel like we're talking in circles, so I'll leave it there. Good luck. – jonrsharpe Jan 10 '20 at 11:46
  • U have clearly more demands as [{"arg":"el1"},{"arg":"el2"},{"arg":"el2"},{"arg":"el3"}] means you also need to have uniquness of elements. The question also why you need here object instead of just array like ['el1', 'el2', 'el3'] ? – Maciej Sikora Jan 10 '20 at 11:57
  • I would prefer the array but found Record which would enforce all the requirements. Will probably have to resort to this. – LeDon Jan 10 '20 at 11:59
  • @MaciejSikora an object is required inside the array as further information will be added to the objects. I omitted it for easier explaination. – LeDon Jan 10 '20 at 12:14

4 Answers4

2

I'm going to define this:

type Arg<T> = T extends any ? { arg: T } : never;

so that we can use Arg<E> (equivalent to {arg:"el1"}|{arg:"el2"}|{arg:"el3"}) in what follows.


The best you can hope for here would be some generic helper function verifyArray() which would enforce the restrictions that its argument is:

  • an array of elements from a union
  • missing no elements from the union
  • and containing no duplicates

And it's going to be ugly.


There's no usable concrete type that will enforce this for unions containing more than about six elements. It is possible to use some illegally-recursive or legally-nonrecursive-but-tedious type definitions to take a union type like 0 | 1 | 2 | 3 and turn it into a union of all possible tuples that meet your criteria. That would produces something like

type AllTuples0123 = UnionToAllPossibleTuples<0 | 1 | 2 | 3>

which would be equivalent to

type AllTuples0123 =
    | [0, 1, 2, 3] | [0, 1, 3, 2] | [0, 2, 1, 3] | [0, 2, 3, 1] | [0, 3, 1, 2] | [0, 3, 2, 1]
    | [1, 0, 2, 3] | [1, 0, 3, 2] | [1, 2, 0, 3] | [1, 2, 3, 0] | [1, 3, 0, 2] | [1, 3, 2, 0]
    | [2, 0, 1, 3] | [2, 0, 3, 1] | [2, 1, 0, 3] | [2, 1, 3, 0] | [2, 3, 0, 1] | [2, 3, 1, 0]
    | [3, 0, 1, 2] | [3, 0, 2, 1] | [3, 1, 0, 2] | [3, 1, 2, 0] | [3, 2, 0, 1] | [3, 2, 1, 0]

But for an input union of n elements that would produce an output union of n! (that's n factorial) outputs, which grows very quickly in n. For your example "el1"|"el2"|"el3" it would be fine:

type AllPossibleTuplesOfArgs = UnionToAllPossibleTuples<Arg<E>>;
const okay: AllPossibleTuplesOfArgs = 
  [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el3" }]; // okay
const bad1: AllPossibleTuplesOfArgs = [{ "arg": "el1" }, { "arg": "el2" }]; // error!
const bad2: AllPossibleTuplesOfArgs = // error!
  [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }];
const bad3: AllPossibleTuplesOfArgs = 
  [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]  // error!

but I assume you want something that doesn't crash your compiler when your object has seven or more properties in it. So let's give up on UnionToAllPossibleTuples and any concrete type.


So what would verifyArray() look like?

First let's make a type function called NoRepeats<T> which takes a tuple type T and returns the same thing as T if and only if T has no repeated elements... otherwise it returns a modified tuple to which T is not assignable. This will allow us to make the constraint T extends NoRepeats<T> to say "the tuple type T has no repeated elements". Here's a way to do it:

type NoRepeats<T extends readonly any[]> = { [M in keyof T]: { [N in keyof T]:
    N extends M ? never : T[M] extends T[N] ? unknown : never
}[number] extends never ? T[M] : never }

So NoRepeats<[0,1,2]> is [0,1,2], but NoRepeats<[0,1,1]> is [0,never,never]. Then verifyArray() might be written as this:

const verifyArray = <T>() => <U extends NoRepeats<U> & readonly T[]>(
    u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
) => u;

It takes a type T to check against, and returns a new function which makes sure its argument has no repeats (from U extends NoRepeats<U>), is assignable to T[] (from & readonly T), and not missing any elements of T (from & ([T] extends [U[number]] ? unknown : never)). Yes it's ugly. Let's see if it works:

const verifyArgEArray = verifyArray<Arg<E>>()
const okayGeneric = verifyArgEArray([{ "arg": "el3" }, { "arg": "el1" }, { "arg": "el2" }]); // okay
const bad1Generic = verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }]); // error
const bad2Generic = // error
    verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }]);
const bad3Generic = // error
    verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]);

So that works.


Both of these force you to fight with the type system. You could possibly make a builder class as in this answer which plays more nicely with the type system but involves even more runtime overhead, and is arguably only slightly less ugly.


Honestly I'd suggest trying to refactor your code not to require TypeScript to enforce this. The easiest thing is to require an object have these values as keys (e.g., just make a value of type T or possibly Record<keyof T, any>) and use that instead of (or before producing) an array. Oh well, hope this helps. Good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for the more than elaborate answer. I already started refactoring to use the route via Record as this can be still iterated with a little more work quite easily. I will mark your answer as correct as while not perfect it answers the question. – LeDon Jan 11 '20 at 16:00
1

You can create a tuple:

type E = "el1"|"el2"|"el3";

type ITestElement<T extends E> = {
    arg: T
};

type ITestElements = [ITestElement<"el1">, ITestElement<"el2">, ITestElement<"el3">];

iY1NQ
  • 2,308
  • 16
  • 18
  • E is of unknown size. So a fixed size tuple won't work here sadly. – LeDon Jan 10 '20 at 11:25
  • If meant to build up an array out of an union type (an array with a fixed length equal to the size of the union types and with no order), that's quite likely not possible: For this purpose a more powerful recursion for types/generics would be needed than typescript supports (at least what i know). – iY1NQ Jan 10 '20 at 12:26
  • There is a way to convert a union type to a tuple of its options, but it's extremely messy and also computationally intractable for larger unions. See https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901 – kaya3 Jan 16 '20 at 18:09
  • The idea is quite brilliant. But the code is hard to handle. – iY1NQ Jan 16 '20 at 19:39
1

I have a solution for enforcing at compile-time that an array contains all values of some arbitrary type T. That's quite close to what you want - it's missing the check to prevent duplicates - but some people may not need to prevent duplicates (e.g. another question marked as a duplicate of this one doesn't have that requirement).

The NoRepeats type from @jcalz's answer can probably be combined with this one if you want to enforce that additional requirement.


We can enforce that an array contains every value of type T by writing a generic function with two type parameters. T is an arbitrary type, and A is the type of the array. We want to check that T[] and A are identical. A nice way to do this is to write a conditional type which resolves to never if these two types are not subtypes of each other:

type Equal<S, T> = [S, T] extends [T, S] ? S : never;

We want to specify the type parameter T explicitly, to tell the compiler what to check for, but we want A to be inferred from the actual value, so that the check is meaningful. Typescript doesn't allow specifying some but not all type parameters of a function, but we can get around this with currying:

function allValuesCheck<T>(): <A extends T[]>(arr: Equal<A, T[]>) => T[] {
    return arr => arr;
}

Then, this function can be used to ask the compiler to check that an array literal contains all possible values of an arbitrary type:

type Foo = 'a' | 'b';

// ok
const allFoo = allValuesCheck<Foo>()(['a', 'b']);
// type error
const notAllFoo = allValuesCheck<Foo>()(['a']);
// type error
const someNotFoo = allValuesCheck<Foo>()(['a', 'b', 'c']);

The downside is that the type errors are not informative; the first error message just says Type 'string' is not assignable to type 'never', and the second error message just says Type 'string' is not assignable to type 'Foo'. So although the compiler informs you of the error, it doesn't tell you which one is missing or erroneous.

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
0

This question together with the other linked questions are concerned about creating an intersection type containing all keys.

I just found an awesome blogpost for this problem and it amazes me about the type system in TypeScript which is more flexible than I thought.

https://fettblog.eu/typescript-union-to-intersection/

The key fact is: TypeScript's type system treats the types of eponymous attributes in united types as an optional intersection of those attribute types or more precisely a power set of intersections and additional arbitrary permutations. Optionality of an attribute's intersected types is expressed by uniting the surrounding object whereas optional intersections require compatible types to be an intersection of all attributes (explained below).

Terminology: covariant types are those which also accept a typecast or a value of a more specific subtype (extended type), contravariant types accept typecasts or values of a more general supertype (base type) and invariant types have no polymorphy (neither subtypes nor supertypes accepted).

Why doesn't the keyof Video.urls example from the blog work? Let's see: If we want to create a manual type clone and keyof would return a union type to represent the power set of intersections for the given attribute then we could try

type Video2 = /*... &*/ { urls: { [key: keyof Video.urls] : string } }

which, due to semantics, would be different from the actually wanted

type Video2 = /*... &*/ { urls: { [key: keyof Format320.urls] : string } } | { urls: { [key: keyof Format480.urls] : string}} | ....

The first one would fail if an instance for Video2 is assigned to a variable of type Video (from the blog). As a simpler example { attr: A | B | (A & B) | (B & A) } is different from {attr: A} | {attr: B}.

And why is that? Type-Safety rules say that a parameter type of a function, eponymous attributes from united types or a generic input type is a contravariant type (position). It means, such a type specifies the most specific type that it can take, not the least specific.

Optional attributes are inherently covariant because they specify one kind of least type (supertype) for all cases. That's why they need another syntax to specify them.

Related to the blog's problem: such a union of intersections, provided to the urls attribute would be a contravariant type while we actually need a covariant type.

What actually works: assigning a value of type { attr: (A & B) | (B & A) } or { attr: A & B } to destination type {attr: A}|{attr: B} ! And that is done by the infer keyword: getting the smallest type which is compatible to the power set of intersections. It must be a subtype of all intersections because the type is in contravariant position (i.e. must be a subtype) and only the intersection of all components is a complete subtype of the power set of intersections.

Dilemma: keyof is expected to represent all possible key-arrangements of a type but there is no way it can return some kind of covariant type for a contravariant type position. That would break the expectations of users.

The practical solution in Typescript is <Name> in keyof <type> which is like an iterator over <type>. You can use it like { [Name in keyof Type] : { Name : number; key : Name }}. Quite sensible feature.

It would be nice if it would work to introduce optional intersections as operators. An example idea: { attr: (A &? B) | (B &? A & C)} which means A and optionally B or A and optionally B and (not optionally) C.

ChrisoLosoph
  • 459
  • 4
  • 8