Let’s try more things.
Always-present props
I’ve tried adding always-present props, like isReady or isFetching and it looks like, as long as every member of Data extends from the base type (directly or indirectly), it works without a type guard:
class BaseData {
isReady: boolean;
isFetching: boolean;
}
class Taxonomy extends BaseData {
isTaxonomy: true;
taxonomy: string;
}
class Search extends BaseData {
isSearch: true;
searchQuery: string;
}
class Category extends Taxonomy {
isCategory: true;
};
type Data = Taxonomy | Search | Category;
// This works fine without type guards:
someData.isFetching = true;
Mix between different types
If we don’t need to add things like TaxonomySearch to Data because it’s enough with separate Taxonomy and Search, then let’s see if we can still mix two types in a single check:
It looks like it can be done with the type guard functions implementation:
type Data = Taxonomy | Search;
function isTaxonomySearch(data: Data): data is Taxonomy & Search {
const d = data as Taxonomy & Search;
return d.isTaxonomy === true && d.isSearch === true;
}
if (isTaxonomySearch(someData)) {
someData.taxonomy = "";
someData.searchQuery = "";
}
For classes, it doesn’t work with a single instanceOf check because it complains that "taxonomy" is not present on type Search. This is what I would have expected:
// This doesn't work
if (someData instanceof Taxonomy && Search) {
someData.taxonomy = ""; // taxonomy doesn't exist on type Search
someData.searchQuery = "";
}
But, surprisingly, it works with two checks or a type guard function:
if (someData instanceof Taxonomy && someData instanceof Search) {
someData.taxonomy = "";
someData.searchQuery = "";
}
function isTaxonomySearch(data: Data): data is Taxonomy & Search {
return data instanceof Taxonomy && data instanceof Search;
}
if (isTaxonomySearch(someData)) {
someData.taxonomy = "";
someData.searchQuery = "";
}
Overwrite a property type
Let’s see what happens if we try to extend from a type, but overwrite one property.
You cannot use an interface to extend a type that is not compatible:
// This doesn't work
interface TaxonomyById extends Taxonomy {
isTaxonomyById: true;
taxonomy: number;
}
You can use types to merge them:
type TaxonomyById = Taxonomy & {
isTaxonomyById: true;
taxonomy: number;
}
But then, you cannot use the property taxonomy anymore, because in the merged type TaxonomyById it gets assigned to the type never:
function isTaxonomyById(data: Data): data is TaxonomyById {
return (data as TaxonomyById).isTaxonomyById === true;
}
// This doesn't work
if (isTaxonomyById(someData)) {
someData.taxonomy = 123;
}
There is a workaround though, using Omit while extending:
interface TaxonomyById extends Omit<Taxonomy, "taxonomy"> {
taxonomy: number;
}
// This works now and taxonomy type is number
if (isTaxonomyById(someData)) {
someData.taxonomy = 123;
}
Adn if you do this, it goes back to never:
// This doesn't work
if (isTaxonomy(someData) && isTaxonomyById(someData)) {
someData.taxonomy = 123;
}
So I guess the workaround is valid.
For classes, it looks like you have to first declare an interface and then use implements to attach the type, like this:
interface TaxonomyById extends Omit<Taxonomy, "taxonomy"> {};
class TaxonomyById implements TaxonomyById {
taxonomy: number;
}
and then interfaceOf works fine:
if (someData instanceof TaxonomyById) {
someData.taxonomy = 123;
}