Hey, I have an idea that would not require devs to extend Data and it seems to work nicely.
Actually, extending Data has a big problem. Imagine that we extend Data to MyData:
type Data = Taxonomy | Post; // The ones you are used to.
type MyData = Data | NewThing; // New data with property `isNewThing`.
export interface SomePackage extends Package {
state: {
source?: {
get: Derived<SomePackage, (link: string) => MyData>;
}
}
//...
}
If you wanted to use the default guard functions that we will have already defined in @frontity/source they wouldn’t work, because the would expect an argument of type Data instead of MyData.
// This is how `isTaxonomy` is supposed to be defined.
// Note that it is using an argument of type `Data`, not `MyData`.
function isTaxonomy(data: Data): data is Taxonomy {
return (data as Taxonomy).isTaxonomy === true;
}
/**
* The next check would return this TypeScript error:
*
* Argument of type 'MyData' is not assignable to parameter of type 'Data'.
* Type 'NewThing' is not assignable to type 'Data'.
* Type 'NewThing' is missing the following properties from type 'Post':
* isPostType, isPost, type, id
*/
const data = state.source.get("/some/link");
if (isTaxonomy(data) { ... } // TypeScript error!
Okay, but what about not using Data anymore?
Let’s say we define these data types: Taxonomy and Post, both extending Base.
interface Base {
isFetching: boolean;
isReady: boolean;
}
interface Taxonomy extends Base {
isTaxonomy: true;
taxonomy: string;
}
interface Post extends Base {
isPostType: true;
isPost: true;
type: "post";
id: number;
}
Instead of using Data in the guard functions, we could use Base.
function isTaxonomy(data: Base): data is Taxonomy {
return (data as Taxonomy).isTaxonomy === true;
}
function isPost(data: Base): data is Post {
return (data as Post).isPost === true;
}
And, for state.source.get(), we could set the return type as Base. That type has the common properties of every data object and you could check isReady or isFetching. If you want to use another property you will need to use a type guard function to cast the data object in the same way as if it had returned an object of type Data, so we are not gaining anything using Data here.
Same with state.source.data. As long as any object you are adding there extends from Base, you could add whatever you want.
export interface Source {
state: {
source: {
// Return an object of type `Base`.
get: Derived<Source, (link: string) => Base>;
// You can store here any object that extends `Base`.
data: Record<string, Base>;
}
}
//...
}
Then, if you want to add your own data types, it would be as simple as creating your own interface and its guard function.
// `NewThing` should extend `Base` (or any other type extending `Base`).
interface NewThing extends Base {
isNewThing: true;
}
// The argument is of type `Base`.
function isNewThing(data: Base): data is NewThing {
return (data as NewThing).isNewThing === true;
}
That’s the only thing you would have to do, and this would work out of the box:
// `data` here is of type `Base`.
const data = state.source.get(link);
// You can check things as `isReady` or `isFetching`.
if (!data.isReady) return null;
// The following type guards will work as expected.
if (isTaxonomy(data)) {
data.isTaxonomy;
data.taxonomy;
} else if (isPost(data)) {
data.id;
data.isPost;
data.isPostType;
} else if (isNewThing(data)) {
data.isNewThing;
}
@dev-team, I would like to hear your feedback 
EDIT: I can’t think of any drawback for this. Let me know if you see any problem.