Home > database >  Enforce Typescript generic type to have keys that start with x
Enforce Typescript generic type to have keys that start with x

Time:11-13

I want to make sure I strong type nested field and enforce that provided type has all keys start with specific text. E.g

type Prefix = `key_${string}`;

type Container<T extends Record<Prefix, any>> = {
  data: T;
};

type ValidType = {
  key_1: string;
  key_2: number;
};

type InvalidType = {
  key_1: string;
  blah: number;
};

// This is allowed - all good.
const container: Container<ValidType> = { 
  data: {
    key_1: 'hello',
    key_2: 123,
  },
};

// This is also allowed - but shouldn't be!
const container_2: Container<InvalidType> = {
  data: {
    key_1: 'hello',
    blah: 231,
  },
};

The behavior I want is whenever someone tries to do Container<InvalidType> typescript should give an error.

UPDATE: Another approach I am thinking to try is

type DataType<T> = {
  [key in keyof T]: T[key];
};

But not sure how to also enforce key above to start with "key_"

CodePudding user response:

We can modify Container to give it a mapped type where we filter out any non-prefixed fields.

type Container<T> = {
  data: {
    [K in keyof T as K extends Prefix ? K : never]: T[K]
  }
};

Assignment of blah will now fail:

// This is allowed - all good.
const container: Container<ValidType> = { 
  data: {
    key_1: 'hello',
    key_2: 123,
  },
};

const container_2: Container<InvalidType> = {
  data: {
    key_1: 'hello',
    blah: 231,
//  ^^^^^^^^^ Error: Type '{ key_1: string; blah: number; }' is not assignable to type '{ key_1: string; }
  },
};

Playground


But be aware! Due to TypeScript's structural type system, excess properties are allowed in most places. We only get an error here because we directly assign an object literal.

To have "real" excess property checks, we need to blacklist the unwanted properties by giving them a never type.

type Container<T> = {
  data: {
    [K in keyof T as K extends Prefix ? K : never]: T[K]
  } & {
    [K in keyof T as K extends Prefix ? never : K]?: never 
  }
};

Now this will fail too:

const container_3 = { 
  data: {
    key_1: 'hello',
    blah: 123,
  },
};

const container_4: Container<InvalidType> = container_3

Playground

  • Related