Search code examples
typescripttypescript-generics

Type error when using Extract to narrow a union


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

Solution

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