google / schema-dts

JSON-LD TypeScript types for Schema.org vocabulary
Apache License 2.0
912 stars 35 forks source link

'@type' as array #189

Open TomRadford opened 1 year ago

TomRadford commented 1 year ago

Was wondering if it would be possible to use the @type in an array. My use case would be in the context of utilizing two types like this: '@type': (['Car', 'Product'] currently working around this by casting to one of the types '@type': (['Car', 'Product'] as unknown as 'Car')

Would be keen to hear if this would be possible?

Eyas commented 1 year ago

Yeah this is also discussed in #179. There are a few issues here.

In general, you can construct your own type as a workaround:

import {Car, Product} from 'schema-dts';

type CarProduct = Omit<Car & Product, '@type'> & { "@type": ["Car", "Product"] }

const c: CarProduct = {
    "@type": ["Car", "Product"],
    "name": "abc",
    "roofLoad": {"@type": "QuantitativeValue"},
};

The problem with generic support, however, is that it's really easy to multiple @types for different "leaf" types, but its harder if we're trying to create types that understand subtypes of each item in the array as well.

curtisburns commented 1 year ago

This doesn't seem to work with ArtGallery and Organization. I've just tried this solution, but apparently, none of the properties exist now - @id, name, sameAs, etc. Is there a workaround for this?

curtisburns commented 1 year ago

For anyone else facing the same issue, I've resorted to the following for now:

type ArtGalleryOrganization = {
  '@type': ['ArtGallery', 'Organization'];
  '@id': Exclude<Organization, string>['@id'];
  name: Exclude<Organization, string>['name'];
  url: Exclude<Organization, string>['url'];
  logo: Exclude<Organization, string>['logo'];
  image: Exclude<Organization, string>['image'];
  founder: Exclude<Organization, string>['founder'];
  description: Exclude<Organization, string>['description'];
  sameAs: Exclude<Organization, string>['sameAs'];
  address: Exclude<Organization, string>['address'];
  location: Exclude<Organization, string>['location'];
  openingHours: Exclude<ArtGallery, string>['openingHours'];
  event: Exclude<Organization, string>['event'];
};

Seeing as ArtGallery is a sub-class of Organization, I only really use the one specific property with the rest coming from Organization.

Also using the Exclude<T, U> method as referencing properties like Organization['sameAs] doesn't seem to work. Obviously not ideal if you need to be aware of all the properties available for Organization and ArtGallery, but not too big of an issue for me. If there's a better way to do this let me know! Cheers.

dsbrianwebster commented 3 weeks ago

1. Here is a work around we've been trying....

(Example Assumes React + Next.JS)

Component:

import { deepmerge } from 'deepmerge-ts';
import type { Thing, WithContext } from 'schema-dts';

export function MultiTypeSchema({ things }: { things: WithContext<Thing>[] }) {
  return (
    <script type='application/ld+json'>
      {JSON.stringify(
        deepmerge(
          ...things.map((thing) => {
            if ('@type' in thing && typeof thing['@type'] === 'string') {
              return {
                ...thing,
                '@type': [thing['@type']],
              };
            }

            return thing;
          }),
        ),
      )}
    </script>
  );
}

Example Usage:

  const siteSchema: WithContext<WebSite> = {
    '@context': 'https://schema.org',
    '@type': 'WebSite',
    name: 'Website Name',
    description: 'Website Description',
    inLanguage: 'en-US',
    url: 'https://website.com',
    potentialAction: {
      '@type': 'SearchAction',
      target: `https://website.com/search?q={search_term_string}`,
      query: 'optional',
    },
    sameAs: ['https://x.com/website'],
  };

  const orgSchema: WithContext<Organization> = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    logo: {
      '@type': 'ImageObject',
      url: 'logo-square.png',
      width: '1024',
      height: '1024',
    },
  };

   <MultiTypeSchema things={[siteSchema, orgSchema]} />

Resulting output 👌:

{
  "@context": "https://schema.org",
  "@type": [
    "WebSite",
    "Organization"
  ],
  "name": "Website Name",
  "description": "Website Description",
  "inLanguage": "en-US",
  "url": "https://website.com",
  "potentialAction": {
    "@type": "SearchAction",
    "target": "https://website.com/search?q={search_term_string}",
    "query": "optional"
  },
  "sameAs": [
    "https://x.com/website"
  ],
  "logo": {
    "@type": "ImageObject",
    "url": "logo-square.png",
    "width": "1024",
    "height": "1024"
  }
}

Image

2. Now the questions that must be asked...

I'm sure there are scenarios where the workaround above is far from a complete solution. For one thing, it makes my LSP start bogging ass, but it does seem to get us the merged result we need.

The only reason we went down this road was to satisfy an SEO specialist a client is working with who insists on multi-type schemas. I'm hoping someone can help us understand what is better about this single multi-type schema versus:

  1. A separate schema for each thing.
  2. Using @graph, which is supported by schema-dts without any hacking or workaround. I would personally prefer to go this way and would like to advocate for it but would like to understand the objective downsides, if any.