Home > other >  Template literal from string in class if string matches certain characteristics
Template literal from string in class if string matches certain characteristics

Time:06-12

I asked this question a bit back, trying to create a class where both a set of keys could be used as args or that set of keys modified with an additional "!".

I landed, consequently, with a class that looks something like this:

export class Schema<PropType extends Record<string, any>> {
  properties: Partial<PropType> = {};

  constructor(properties: PropType) {
    this.properties = properties;
  }

  /**
   * Select specific fields *among those already selected*.
   */
  pick = <K extends keyof PropType | `${Extract<keyof PropType, string>}!`>(
    ...fields: Array<K>
  ) => {
    const newProperties = {} as Record<keyof PropType, unknown>;
    fields.forEach((field) => {
      const { name, required } = this._parse(field);
      newProperties[name] = {
        ...(this.properties[name]),
        required,
      };
    });

    return new Schema<Record<K, unknown>>(
      newProperties
    );
  };

    /**
   * Parse a field string, returning the base field "name", and
   * whether the field is required (if it ends in a !).
   */
  _parse = (field: keyof PropType | `${Extract<keyof PropType, string>}!`) => {
    return {
      name: (field as string).replace("!", "") as keyof PropType,
      required: (field as string).split("").reverse()[0] === "!",
    };
  };
}

The pick function is where the magic happens. It lets me pick both 'keyName' and 'keyName!' if 'keyName' is one of the keys of PropType. In my case, the '!' means that the key is required.

const Person = new Schema({ firstName: 'Fyodor', middleName: 'Mikhailovich', lastName: 'Dostoevsky' });

const SubPerson = Person.pick('firstName', 'lastName!');

The trouble is, if I "layer" pick calls, it adds on exclamation points:

SubPerson.pick(
  'firstName', 
  'lastName!', 
  'lastName!!' // Uh-oh! This shouldn't be acceptable
)

See the playground. Is there a way to allow ${Extract<keyof PropType, string>}! IFF keyof PropType doesn't already have a "!"?

CodePudding user response:

If you want to detect string literal types that end in "!", you can use a "pattern literal type" as implemented in microsoft/TypeScript#40598:

type EndsWithExclamation = `${string}!`
let v: EndsWithExclamation;
v = "okay!" // okay
v = "problem?" // error

So you could just use the Exclude<T, U> utility type to filter any strings ending in "!" out of keyof T:

type QuietKeys<T extends object> = Exclude<keyof T, `${string}!`>;
type X = QuietKeys<{ firstName: unknown, lastName: unknown, "lastName!": unknown }>;
// type X = "firstName" | "lastName"

Like this:

pick = <K extends keyof T | `${Extract<Exclude<keyof T, `${string}!`>, string>}!`>(
  ...fields: Array<K>
) => {

But, you probably don't really want to do that. The return type of pick() is Schema<Record<K, unknown>>. So if you pass in a field like "firstName!", then the output type will be Schema<{"firstName!": unknown}>. But if the actual field name was firstName, then you'd want to see Schema<{firstName: unknown}> instead. You don't want to output the exclamation point.


The way I'd do this is to key K extends keyof T only, so that you know that K will definitely be keys from the original object type and not the ones with ! added. Like this:

pick = <K extends keyof T>(
  ...fields: Array<K | `${Extract<K, string>}!`>
) => {

So now fields can have exclamations added, but not K. That gives us this behavior:

const SubPerson = Person.pick('firstName', 'lastName!');
// const SubPerson: Schema<Record<"firstName" | "lastName", unknown>>

SubPerson.pick(
  'firstName',
  'lastName!',
  'lastName!!' // error
);

Where lastName!! is rejected, not because the underlying key already ended in an exclamation point, but because the underlying key is just "lastName".

Playground link to code

  • Related