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;
}