Home > Software design >  Typescript T[keyof T] of type string
Typescript T[keyof T] of type string

Time:04-13

Demo: https://tsplay.dev/mMM8Vm

I got an array of T and that has keys of the type string. So I want to iterate over all entries in the array and trim the string.

Error: Property 'trim' does not exist on type 'T[Extract<keyof T, string>]'

ifAlreadyExists<T>(
    data: Array<T>,
    key: Extract<keyof T, string>,
  ): boolean {

      return (
        data.filter(
          (item) => {
            const v = item[key];

            return v.trim().toLowerCase()
          }
        ).length === 0)
  }

How can I tell Typescript that item[key] will be of the type string?

CodePudding user response:

As kaya3 mentioned, your signature constrains key to string keys, rather than to keys that have string values. To do the latter, you can introduce a type variable Key (which can be any key type), and constrain the array elements to objects that have a string value for the key denoted by Key (i.e. {[K in Key]: string}):

  ifAlreadyExists<Key extends PropertyKey>(
    data: Array<{[K in Key]: string}>,
    key: Key,
  ): boolean {
    ..
  }

TypeScript playground

CodePudding user response:

If you want to inspect a given collection if a given property of any item is holding a specific value maybe this might do the trick

type Book = {
  id: string;
  title: string;
};

class AppComponent {

  constructor() {
    const bookList: Array<Book> = [{ id: '111', title: 'Title 1' }, { id: '222', title: 'Title 2' }];
    this.propertyValueExists(bookList, 'title', 'Title 2');
  }

  propertyValueExists<Key extends PropertyKey>(
    collection: Array<{[K in Key]: string}>,
    propertyKey: Key,
    expectedValue: string
  ): boolean {
    return collection.some(item => {
      const actualValue = item[propertyKey];

      return actualValue.trim().toLowerCase() === expectedValue;
    });
  }
}

new AppComponent();

Example:

https://www.typescriptlang.org/play?#code/C4TwDgpgBAQg9nA1lAvFA3gKClAlgEwC4oBnYAJ1wDsBzAbmymF2ABsJizLaGBfBzAGNWAQxIkoAQTBgAwnAC2YOFQhVgGTI0EquAV0HA45ABQBKTThw6qZKACMEiADK4yxSeXIiQAHnhIAHyoUADa6HhEUADkAIzx0QA0TCzsxNEAKqnQsdFQvMkRBOkATGVJKWwcMVlVUCV5vAC6DFZMABZuAHRg5HCQ5KAAaiKsehAAogAebsAkJo5IrmTJ0cxVFZnZ9dFmrflaOL39EIMgI2OTM2QkvgDSECBQEFPAavgSAAp9A6APIIETIxrHBWOxDLgVB4vD5fOhQnc8FQoP8mpwKNQaLxAolgVBjr8QP9iP9cW0XpBDBB8BdxujuDRGGZiIt2CJkVg2uQIMA9ORkTowRAISouiRFBATCwIApUMFOW0oDY7CJDHpRrToGhpQpQgTTn9Hi1Dorubz VBVbyNaNxl0MQpzPa4M44AB3U6yMSSiwoP3PKaUt4020QfY4Xh7Ri8TAxzCqN1SGTyJQqNTAcx0IA

CodePudding user response:

Ignoring the boolean vs string return value in the filter and deliberately not constraining T to be a Record<PropertyKey, string>.

Use a type-guard:

  return (
    data.filter(
      (item) => {
        const v = item[key];
        if (typeof v === 'string') {
          return v.trim().toLowerCase();
        }
        return 'something?';
      }
    ).length === 0)

For extra points you may want to only expose keys for which the implementation is valid.

Only produces a type where for each key of T its corresponding field is assignable to U.

type Only<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never
}[keyof T];

then

ifAlreadyExists<T>(
  data: Array<T>,
  key: Only<T, string>, // only keys where values are string-like
): boolean {
  ...
}

And there are values other than strings in the Book shape.

type Book = {
  id: string;
  title: string;
  isbn: number;
  genre: 'fantasy' | 'horror'
};

const bookList: Array<Book> = [{ id: '222', title: 'Title 1', isbn: 1237681231, genre: 'fantasy' }];
this.ifAlreadyExists(bookList, "id"); // fine
this.ifAlreadyExists(bookList, "title"); // fine
this.ifAlreadyExists(bookList, "genre"); // fine
this.ifAlreadyExists(bookList, "isbn"); // Error! isbn is not string

Playground

  • Related