Tiago Fernandes
Tiago Fernandes

Reputation: 35

Create a type that is an array of an object properties

So I'm trying to get this behavior:

interface A {
    prop1: string;
    prop2: number;
    prop3: boolean;
}

type PropertyArray<T> = magical code

// PropertyArray<A> should be the same as the type ['prop1', 'prop2', 'prop3']

const properties: PropertyArray<A> = ['prop1', 'prop2', 'prop3'];

const fail1: PropertyArray<A> = ['prop1', 'prop2']; // type error
const fail2: PropertyArray<A> = ['prop1', 'prop1', 'prop2', 'prop3']; // type error

The point of this code is that if I add a new field the interface A, I also need to include that property to the array. If any of the properties is missing the build fails.

Thanks for the help.

Upvotes: 2

Views: 102

Answers (3)

jcalz
jcalz

Reputation: 327964

If you really care about property order then this is essentially a duplicate of this question about turning a union into a tuple if you define

type PropertyArray<T> = TuplifyUnion<keyof T>

But hopefully you don't really care about property order, and ['prop2', 'prop1', 'prop3'] would be an acceptable value of properties. In that case, there are two ways I can think of doing this:


One is to actually calculate PropertyArray<T> as a union of all possible permutations of keys in tuples, as you asked. This would naturally involve a circular conditional type which is not currently supported. I can instead make a definition that supports types with up to some fixed number of properties, like this:

type PropertyArray<T> = Tup<keyof T>
type Cons<H, T extends any[]> = T extends any ? ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never
type Tup<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup1<Exclude<V, U>>> : never
type Tup1<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup2<Exclude<V, U>>> : never
type Tup2<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup3<Exclude<V, U>>> : never
type Tup3<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup4<Exclude<V, U>>> : never
type Tup4<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup5<Exclude<V, U>>> : never
type Tup5<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup6<Exclude<V, U>>> : never
type Tup6<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup7<Exclude<V, U>>> : never
type Tup7<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup8<Exclude<V, U>>> : never
type Tup8<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, Tup9<Exclude<V, U>>> : never
type Tup9<U, V = U> = [U] extends [never] ? [] : U extends any ? Cons<U, TupX<Exclude<V, U>>> : never
type TupX<U> = [] // bail out

And for your case:

interface A {
    prop1: string;
    prop2: number;
    prop3: boolean;
}

const properties: PropertyArray<A> = ['prop1', 'prop2', 'prop3'];
const fail1: PropertyArray<A> = ['prop1', 'prop2']; // type error
const fail2: PropertyArray<A> = ['prop1', 'prop1', 'prop2', 'prop3']; // type error

This works how you want, but is a lot of work for the compiler and could be brittle.


A slightly less crazy solution (which is still kind of crazy) is to use a helper function instead of a type alias. The helper function will only compile if its parameters include each and every key of the relevant object type exactly once:

type TupleHasRepeats<T extends any[]> = { [I in keyof T]: T[I] extends T[Exclude<keyof T, keyof any[] | I>] ? unknown : never}[number] 

const propertyArray = <T>() => <A extends Array<keyof T>>(...a: A & (keyof T extends A[number] ? unknown : never) & (unknown extends TupleHasRepeats<A> ? never : unknown )) => a;

And then try it:

interface A {
    prop1: string;
    prop2: number;
    prop3: boolean;
}

const propertyArrayA = propertyArray<A>();

const properties = propertyArrayA('prop1', 'prop2', 'prop3');
const fail1 = propertyArrayA('prop1', 'prop2'); // type error;
const fail2 = propertyArrayA('prop1', 'prop1', 'prop2', 'prop3'); // type error

That works also. I'd probably use the latter if I had to do anything in production code.


Okay, hope that helps; good luck!

Upvotes: 1

pzaenger
pzaenger

Reputation: 11973

Use keyof:

type PropertyArray = Array<keyof A>; // 'prop1' | 'prop2' | 'prop3'
type PropertyArray<T> = Array<keyof T>;

Upvotes: 1

Murtaza Hussain
Murtaza Hussain

Reputation: 4285

If you need an array of A interface then simply do the following:

const properties: A[] = [ { 'string', 0, true }, { 'string', 1, false } ];

Let me know if I understand your question right.

Thanks

Upvotes: 0

Related Questions