Reputation: 75
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.
Upvotes: 4
Views: 1499
Reputation: 6799
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);
}
}
Upvotes: 2