xp
xp copied to clipboard
TypeScript type for Union of Content #9808
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.
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.
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 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">
@tajakobsen I'm okay with functions that operates on a single element (create
, modify
, move
) not using ContentUnion
.
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?
Each time you light your lighter your lighter gets lighter until your lighter gets so light that it won't light
Why do one have to say the same thing twice?
- The first field in the tuple gives the shape of
content.data
, so that we can program against it. - 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
Nice. I've only tested the query.
Hovering hit it looks perfect:
data.repository also looks perfect:
Weird that data looks perfect here:
But rather complex here:
Maybe just my editor?