Search code examples
typescriptconditional-types

Typescript conditional type missing properties


I can't wrap my head around why the following TypeScript code fails while seemingly everything should be ok:

interface Base { basic: boolean };
interface Super { basic: boolean; extra: boolean };

type Options<T> = T extends Super ? { isSuper: boolean } : { notSuper: boolean }

const baseOptions: Options<{ basic: boolean }> = { notSuper: true };                 // ok
const superOptions: Options<{ basic: boolean; extra: boolean }> = { isSuper: true }; // ok

type MakeOptions = <T>() => Options<T>

function withOptions <T>(makeOptions: MakeOptions): void {
  const options = makeOptions<T>();

  console.log(options.notSuper); // Error: Property 'notSuper' does not exist on type 'Options<T>'.(2339)
  console.log(options.isSuper);  // Error: Property 'isSuper' does not exist on type 'Options<T>'.(2339)
}

I expect options.isSuper to be undefined | { isSuper: boolean } and options.notSuper to be undefined | { notSuper: boolean }

Instead Typescript removes these properties alltogether.

Problem is solved when changing to

type Options<T> = T extends Super ? { isSuper: boolean; notSuper?: undefined } : { notSuper: boolean; isSuper?: undefined }

But it seems unnecessary.

Playground


Solution

  • In Options<T> as you want it to return an object with the parameter isSuper or notSuper I added in an interface for both of them.

    interface IisSuper { isSuper: boolean }
    interface InotSuper { notSuper: boolean }
    

    In Options<T>as it can either be one of the aforementioned interfaces I created a union type for it called TSuper.

    type Options<T> = T extends Super ? IisSuper : InotSuper
    type TSuper = IisSuper | InotSuper
    

    In the function withOptions<T> I used the as keyword, which is a Type Assertion which tells the compiler to consider the object as another type than the type the compiler infers the object to be. In this case it is the union of both IisSuper and InotSuper which Options<T> can exist as.

    As Typescript cannot guarantee the type of options at runtime as you want to access either notSuper or isSuper so you have to narrow down the scope using the in keyword for options to access the parameter in the type you want.

    function withOptions <T>(makeOptions: MakeOptions): void {
      const options = makeOptions<T>() as TSuper;
      if('notSuper' in options){
        console.log(options.notSuper);
      }
      else if('isSuper' in options){
        console.log(options.isSuper);
      } 
    }
    

    Final code:

    interface Base { basic: boolean };
    interface Super { basic: boolean; extra: boolean };
    interface IisSuper { isSuper: boolean }
    interface InotSuper { notSuper: boolean }
    
    type Options<T> = T extends Super ? IisSuper : InotSuper
    type MakeOptions = <T>() => Options<T>
    type TSuper = IisSuper | InotSuper
    
    const baseOptions: Options<{ basic: boolean }> = { notSuper: true };                 
    const superOptions: Options<{ basic: boolean; extra: boolean }> = { isSuper: true }; 
    
    function withOptions <T>(makeOptions: MakeOptions): void {
      const options = makeOptions<T>() as TSuper;
      if('notSuper' in options){
        console.log(options.notSuper);
      }
      else if('isSuper' in options){
        console.log(options.isSuper);
      } 
    }