Home > Mobile >  Create constant array type from an object type
Create constant array type from an object type

Time:10-22

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'

CodePudding user response:

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

CodePudding user response:

This is conceptually the opposite of what you asked in that it allows us to create an object using the array as a type rather than setting restrictions on an array based on a given object type. But the interdependence is bidirectional so you might also be able to make use of it.

Create a type from your const array, then create a mapping type using that as the key type. If you forget to set one of the const array values on the object or set one that isn't in the array, you'll get an error.

const categoryNames = ['a', 'b', 'c', 'd'] as const

export type Keys = typeof categoryNames[number]

export type Categories<Key extends string, Type> = {
  [name in Key]: Type
}

const mapping: Categories<Keys, number> = {
  a: 0,
  // b: 1, with 'b' commented out there will be an error
  c: 2,
  d: 3
}
  • Related