Home > front end >  TypeScript object set with unique property
TypeScript object set with unique property

Time:10-24

i wrote simply interfaces like that:

interface IProduct {
  id: number;
  name: string;
  price: number;
  description?: string;
}

Now i want to id be unique in ReadonlyArray. So when a create a few products, i want to prevent add object with the same id. Array with products will be creating once, in file, and will not be modified.

Have you any idea for that? JS Set will be good solution, but i can't add own comparator to them. Please not provide solution which require additional frameworks etc.

CodePudding user response:

This example uses type system to staticaly validate whether there are duplicates or not.


interface IProduct<Id extends number> {
    id: Id
    name: string;
}

const product = <Id extends number>(id: Id, name: string) => ({ id, name })

type Validation<
    Products extends IProduct<number>[],
    Accumulator extends IProduct<number>[] = []>
    =
    (Products extends []
        // #1 Last call
        ? Accumulator
        // #2 All calls but last
        : (Products extends [infer Head, ...infer Tail]
            ? (Head extends IProduct<number>
                // #3 Check whether [id] property already exists in our accumulator 
                ? (Head['id'] extends Accumulator[number]['id']
                    ? (Tail extends IProduct<number>[]
                        // #4 [id] property is a duplicate, hence we need to replace it with [never] in order to trigger the error
                        ? Validation<Tail, [...Accumulator, { id: never, name: Head['name'] }]>
                        : never)
                    // #5 [id] is not a duplicate, hence we can add to our accumulator whole product
                    : (Tail extends IProduct<number>[]
                        ? Validation<Tail, [...Accumulator, Head]>
                        : never)
                )
                : never)
            : never)
    )

type Ok = Validation<[{ id: 1, name: '1' }, { id: 2, name: '2' }]>
type Fail = Validation<[{ id: 1, name: '1' }, { id: 1, name: '2' }]> // id:never

const builder = <
    Product extends IProduct<number>,
    Products extends Product[]
>(...products: [...Products] & Validation<Products>) => products

builder(product(1, 'John'), product(2, 'Doe'))

Playground

Validation - iterates recursively through all passed into function products. If product[id] already exists in accumulator type - replace id property with never, otherwise just add product to accumulator.

Please see the comments #1, #2 ....

If you dont want to use rest operator, consider this example:


interface IProduct<Id extends number> {
    id: Id
    name: string;
}

const product = <Id extends number>(id: Id, name: string) => ({ id, name })

type Validation<
    Products extends IProduct<number>[],
    Accumulator extends IProduct<number>[] = []>
    =
    (Products extends []
        // #1 Last call
        ? Accumulator
        // #2 All calls but last
        : (Products extends [infer Head, ...infer Tail]
            ? (Head extends IProduct<number>
                // #3 Check whether [id] property already exists in our accumulator 
                ? (Head['id'] extends Accumulator[number]['id']
                    ? (Tail extends IProduct<number>[]
                        // #4 [id] property is a duplicate, hence we need to replace it with [never] in order to trigger the error
                        ? Validation<Tail, [...Accumulator, { id: never, name: Head['name'] }]>
                        : 1)
                    // #5 [id] is not a duplicate, hence we can add to our accumulator whole product
                    : (Tail extends IProduct<number>[]
                        ? Validation<Tail, [...Accumulator, Head]>
                        : 2)
                )
                : 3)
            : Products)
    )


type Ok = Validation<[{ id: 1, name: '1' }, { id: 2, name: '2' }]>
type Fail = Validation<[{ id: 1, name: '1' }, { id: 1, name: '2' }]> // id:never

const builder = <
    Id extends number,
    Product extends IProduct<Id>,
    Products extends Product[]
>(products: Validation<[...Products]>) => products

builder([product(1, 'John'), product(1, 'John')]) // error

Playground

If you are interested in static validation, you can check my article

CodePudding user response:

Do you want compile-time guarantees for this? I doubt this would be possible in TypeScript. Edit: Maybe it is possible after seeing the other answer.

However, the JavaScript code is quite simple:

let products = new Map();

let product = {
  id: 1,
  name: "Foo",
  price: 99,
  description: "Bar",
};

// This will update an existing item if it has the same ID
products.set(product.id, product);

// Alternatively, you can check if one already exists in keep the original item
if (!products.has(product.id)) {
  products.set(product.id, product);
}

You could wrap this code in your own class with a set of functions:

class ProductList {
  constructor() {
    this.items = new Map();
  }

  add(product) {
    if(!this.items.has(product.id)) {
      this.items.set(product.id, product);
    }
  }

  values() {
    // iterate over values
    // this will respect insertion order
    return this.items.values();
  }

  // any other methods you'd like...
}

If you need random access by index, you could store an array in your class which is kept in-sync/updated alongside your set. This shouldn't be too difficult to code up.

You were mentioning ReadonlyArray. I think you can try to make your class implement this interface, if that's something you desire.

  • Related