Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create constant array type from an object type

Tags:

typescript

I have an object type similar to this:

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

I also have a readonly array similar to this:

// Type: readonly ["countryCode", "currency", "otherFields"]
const allowedFields = ["countryCode", "currency", "otherFields"] as const;

I want to be able to specify an interface for this array declaration based on the Fields object type so that any change to it will require to change the array as well. Something like this:

// How to create 'SomeType'?
const allowedFields: SomeType = ["countryCode"] as const; // Should throw error because there are missing fields

const allowedFields: SomeType = ["extraField"] as const; // Should throw error because "extraField" is not in the object type 'Fields'
like image 594
Mauricio Dziedzinski Avatar asked Oct 28 '25 18:10

Mauricio Dziedzinski


2 Answers

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
  [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];


type AllowedFields = TupleUnion<keyof Fields>;


const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];


// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields

const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

You need to create a permutation of all allowed props. Why permutation ? Because keys of dictionary are unordered.

Playground

EXPLANATION

Let's get rid of recursive call and conditional type:

{
  type TupleUnion<U extends string, R extends any[] = []> = {
    [S in U]: [...R, S]
  }

  type AllowedFields = TupleUnion<keyof Fields>;
  type AllowedFields = {
    countryCode: ["countryCode"];
    currency: ["currency"];
    otherFields: ["otherFields"];
  }
}

We have created an object, where each value is a tuple with key. In order to get things done, each value, should contain each key in different order. Smth like that:

  type AllowedFields = {
    countryCode: ["countryCode", 'currency', 'otherFields'];
    currency: ["currency", 'countryCode', 'otherFields'];
    otherFields: ["otherFields", 'countryCode', 'currency'];
  }

Hence, in order to add two other props, we need to call TupleUnion recursively, but without an element which already exists in a tuple. It means, that our second call should do this:


  type AllowedFields = {
    countryCode: ["countryCode", Exclude<Fields, 'countryCode'>];
    currency: ["currency", Exclude<Fields, 'currency'>];
    otherFields: ["otherFields", Exclude<Fields, 'otherFields'>];
  }

To achieve, it we need do this: TupleUnion<Exclude<U, S>, [...R, S]>;. Maybe it will be much readable if I write:

type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }

But then we will gent deep nested data structure:

  type AllowedFields = TupleUnion<keyof Fields>['countryCode']['currency']['otherFields']

We should not call TupleUnion recursion if Exclude<U, S>, or in other words Exclude<FieldKeys, Key> returns never. We need to check if Key is a last property. In order to do that, we can check if Exclude<U, S> extends never. IF it is never - no more keys, we can just return [...R,S].

I hope that this code:

{
  type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }

  type AllowedFields = TupleUnion<keyof Fields>

}

is much clearer. However, we still have an object with values instead of tuple. Each value in object is a tuple of desired type. In order to get a union of all values, we just need to use square bracket notation with union of all keys. Smth like that: type A = {age:1,name:2}['age'|'name'] // 1|2.

Final code:

 type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }[FieldKeys] // added suqare bracket notation with union of all keys
like image 85
captain-yossarian Avatar answered Oct 31 '25 09:10

captain-yossarian


Using code from https://stackoverflow.com/a/70061272 should avoid Type instantiation is excessively deep and possibly infinite (ts2589) in @captain-yossarian answer (example) :

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

type UnionToParm<U> = U extends any ? (k: U) => void : never;
type UnionToSect<U> = UnionToParm<U> extends (k: infer I) => void ? I : never;
type ExtractParm<F> = F extends { (a: infer A): void } ? A : never;

type SpliceOne<Union> = Exclude<Union, ExtractOne<Union>>;
type ExtractOne<Union> = ExtractParm<UnionToSect<UnionToParm<Union>>>;

type ToTuple<Union> = ToTupleRec<Union, []>;
type ToTupleRec<Union, Rslt extends any[]> = SpliceOne<Union> extends never
  ? [ExtractOne<Union>, ...Rslt]
  : ToTupleRec<SpliceOne<Union>, [ExtractOne<Union>, ...Rslt]>;

type AllowedFields = ToTuple<keyof Fields>;


const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];


// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields

const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

Playground

like image 25
AidenLx Avatar answered Oct 31 '25 09:10

AidenLx