I am trying to narrow the return type of my EthereumViewModel.getCoinWithBalance
by using the Extract
utility type to take part of my FlatAssetWithBalance
union based on EthereumViewModel
's generic C
type (which is constrained to 'eth' | 'polygon'
), but it doesn't seem to overlap with the types of my getAsset
function and therefore results in an error on the return
line of my getCoinWithBalance
function.
How can I change my getCoinWithBalance
and or getAsset
functions so that the types correctly overlap, and can you explain why they don't currently overlap?
You can see this example on the TypeScript Playground here, but I have also copied the code and error message below.
type ExpandRecursively<T> = T extends object
? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
: T;
type ChainId = "eth" | "btc" | "polygon";
type EthereumChainId = Exclude<ChainId, "btc">;
type BlockchainInfo = {
chainId: "btc";
} | {
chainId: "eth";
contract: string;
} | {
chainId: "polygon";
contract: string;
}
type ExtractChain<A extends BlockchainInfo, C extends ChainId> = Extract<A, { chainId: C }>;
type BaseAsset = {
name: string;
symbol: string;
id?: number | undefined;
image?: string | undefined;
rank?: number | undefined;
decimals: number;
amount: number;
}
type FlatAssetWithBalance = BaseAsset & {
chainId: "btc";
} | BaseAsset & {
contract: string;
chainId: "eth";
} | BaseAsset & {
contract: string;
chainId: "polygon";
}
type EthChainFlatAssetWithBalance<C extends EthereumChainId> = ExtractChain<FlatAssetWithBalance, C>;
type EthChainFlatAssetWithBalanceTest = EthChainFlatAssetWithBalance<'polygon'>;
declare const getAsset: <C extends EthereumChainId>(chainId: C) => BaseAsset & { chainId: C; contract: string; };
class EthereumViewModel<C extends EthereumChainId> {
chainId: C = 'eth' as C; // Just for the sake of testing
getCoinWithBalance(): EthChainFlatAssetWithBalance<C> {
// the below line is where the type error displays
return getAsset(this.chainId);
}
}
Here's the full error thrown from the return line of getCoinWithBalance
.
Type 'BaseAsset & { chainId: C; contract: string; }' is not assignable to type 'EthChainFlatAssetWithBalance<C>'.
Type 'BaseAsset & { chainId: C; contract: string; }' is not assignable to type 'Extract<BaseAsset & { contract: string; chainId: "polygon"; }, { chainId: C; }>'.
The issue is that your generic C
type, although constrained to your EthereumChainId
type ('eth' | 'polygon'
) is not necessarily only one of the types from that union. It may include both 'eth'
and 'polygon'
.
This causes an issue with the return type of getCoinWithBalance
because the return type of getAsset
never returns a union, but the Extract
type (used by getCoinWithBalance
) can return a union of BaseAsset & { contract: string; chainId: "eth"; } | BaseAsset & { contract: string; chainId: "polygon"; }
(because C
may include both chain IDs).
To fix your code, you simply need to update the return type of getAsset
to use the same Extract
utility type e.g.
declare const getAsset: <C extends EthereumChainId>(chainId: C) => ExtractChain<FlatAssetWithBalance, C>;