xp icon indicating copy to clipboard operation
xp copied to clipboard

TypeScript type for Union of Content #9808

Open tajakobsen opened this issue 2 years ago • 1 comments

The return type of contentLib.query() (and similar functions) should have the shape Content<A, 'a'> | Content<B, 'b'> instead of the current implementation which is Content<A | B, 'a' | 'b'>.

This change allows TypeScript to infer the correct type of content by using a simple if-statement against Content.type, which is the natural way for an application programmer to do this.

tajakobsen avatar Oct 20 '22 10:10 tajakobsen

I have not changed the shape of the functions that create or modify or move content. They basically operate on a single content that we know the type of, and doesn't have to deal with ContentUnion.

The consequence is that some functions take a tuple with the shape of the content, and the name of the type – and some functions just take in the shape and the name as two parameters.

I'm happy with this. But I would like to hear what you are thinking about it.

tajakobsen avatar Oct 20 '22 10:10 tajakobsen

I tried to use it here: https://github.com/enonic/site-developer/blob/issue-891/typeScript/src/main/resources/site/parts/cards/cards.ts#L66-L70

But it says:

src/main/resources/site/parts/cards/cards.ts:67:3 - error TS2344: Type 'ContentTypeArticle | ContentTypeDoc | ContentTypeGuide' does not satisfy the constraint 'readonly [data: unknown, type: string]'.
  Type 'ContentTypeArticle' is not assignable to type 'readonly [data: unknown, type: string]'.

Haven't studied it closely, running to lunch.

ComLock avatar Oct 21 '22 09:10 ComLock

@ComLock That was actually the main point to return the correct Content type. BTW you are passing the Content, we actually accept Data and Type.

Previously we had this:

// definition
export function query<Data = Record<string, unknown>, Type extends string = string>(params: QueryContentParams): ContentsResult<Data, Type> { ... }
// usage
query<Article | Employee, "article" | "employee">({query: '...'}); 
// will return result with Content<Article | Employee, "article" | "employee">
// This is not correct!

Now we have this:

// definition
export function query<DataAndType extends DataAndTypeTuple = readonly [data: Record<string, unknown>, type: string]>(params: QueryContentParams): ContentsResult<DataAndType> { ... }
// usage
query<[Article, "article"] | [Employee, "employee"]>({query: '...'});
// will return result with Content<Article, "article"> | Content<Employee, "employee">

edloidas avatar Oct 21 '22 11:10 edloidas

@tajakobsen I'm okay with functions that operates on a single element (create, modify, move) not using ContentUnion.

edloidas avatar Oct 21 '22 11:10 edloidas

Why do one have to say the same thing twice? Both the type and the type of the type? Doesn't the type contain it's type? Just use a type guard on the result?

ComLock avatar Oct 24 '22 07:10 ComLock

Each time you light your lighter your lighter gets lighter until your lighter gets so light that it won't light

ComLock avatar Oct 24 '22 07:10 ComLock

Why do one have to say the same thing twice?

  1. The first field in the tuple gives the shape of content.data, so that we can program against it.
  2. The second field of the tuple contains the name of the content type which sets content.type to the string literal (e.g "myapp:Article"), which allows the application developer to use a simple if-statement to separate the union of possible types the result can be.

The combination of content.data and content.type lets the application developer – in a typesafe way – write TypeScript-code that is very close to the natural JavaScript-code, but with full type safety:

Imagine a part that can be used on two content types, Article and Employee:

interface Article {
  title: string;
}

interface Employee {
  fullName: string;
}

// the type parameter does the heavy lifting in describing the possible shapes of content
const content = getContent<[Article, 'myapp:Article'] | [Employee, 'myapp:Employee']>();

/*
This gives a shape similar to this for content:

type MyContent = 
  | {
    type: 'myapp:Article',
    data: {
      title: string
    }
  }
  | {
    type: 'myapp:Employee',
    data: {
      fullName: string;
    }
  }

*/
// This gives us a discriminated union on `content.type`: https://css-tricks.com/typescript-discriminated-unions/#aa-discriminated-unions

const title = (content.type === "myapp:Article")
  ? content.data.title  // Because of the tuple TypeScript knows the shape the `data` that belongs to "myapp:Article" 
  : content.data.fullName; // And it knows if it isn't "myapp:Article", it has have the shape of "myapp:Employee"

// no other fields on `content.data` would be allowed to be used

tajakobsen avatar Oct 24 '22 08:10 tajakobsen

Nice. I've only tested the query.

Hovering hit it looks perfect: Screenshot 2022-10-24 at 14 32 20

data.repository also looks perfect: Screenshot 2022-10-24 at 14 34 12

Weird that data looks perfect here: Screenshot 2022-10-24 at 14 36 04

But rather complex here: Screenshot 2022-10-24 at 14 36 40

Maybe just my editor?

ComLock avatar Oct 24 '22 12:10 ComLock