0

How do I restrict the keyof to only accept keyof where the type is of specified type?

interface Data {
  items: string[];
  name: string;
}

// so far this accepts any valid key of `T`.
// I want to restrict this so that it only allows keys where the value of the key is type `F`
type Key<T, F = any> = keyof T & string;

interface TextField<T> {
   input: 'text'
   key: Key<T, string>
}

interface ArrayField<T> {
  input: 'text',
  key: Key<T, Array>
}

type Field<T> = TextField<T> | ArrayField<T>;

// Should NOT error
const config: Field<Data>[] = [
   {
      input: 'text'
      key: 'name'
   },
   {
      input: 'array',
      key: 'items'
   }
];

// Should error because the items is not a string and name is not an array
const config: Field<Data>[] = [
   {
      input: 'text'
      key: 'items'
   },
   {
      input: 'array',
      key: 'name'
   }
]
Ewan
  • 378
  • 3
  • 14

1 Answers1

0

What you are looking for is a mapped type with a combination with key remapping. key remapping will allow us to only keep the desired properties:

type Key<T, F = any> = keyof {
  [K in keyof T as T[K] extends F ? K : never]: T[K];
};

Testing:

interface Data {
  items?: string[];
  name: string;
}

// "name"
type Case1 = Key<Data, string>

To get any array type we will use readonly unknown[] as a type because read-only arrays are superset for mutable arrays:

// false
type Case1 = readonly number[] extends number[] ? true : false
// true
type Case2 = number[] extends readonly number[] ? true : false

Testing:

// type Case2 = never
type Case2 = Key<Data, readonly unknown[]>

never? Interesting, items surely match the type, however, the problem is that items property is optional. And optional objects are supersets for required ones.

// false
type Case1 = { a?: string } extends { a: string } ? true : false;
// true
type Case2 = { a: string } extends { a?: string } ? true : false;

To fix it, we will need to use the built-in NonNullable utility type:

// string[] | undefined
type Case1 = Data['items']
// string[]
type Case2 = NonNullable<Data['items']>

Updating Key and other types:

type Key<T, F = any> = keyof {
  [K in keyof T as NonNullable<T[K]> extends F ? K : never]: T[K];
};

interface TextField<T> {
  input: 'text';
  key: Key<T, string>;
}

interface ArrayField<T> {
  input: 'array';
  key: Key<T, readonly unknown[]>;
}

Final testing:

// no error
const config: Field<Data>[] = [
  {
    input: 'text',
    key: 'name',
  },
  {
    input: 'array',
    key: 'items',
  },
];

// error because the items is not a string and name is not an array
const config2: Field<Data>[] = [
  {
    input: 'text',
    key: 'items',
  },
  {
    input: 'array',
    key: 'name',
  },
];

playground

wonderflame
  • 6,759
  • 1
  • 1
  • 17