简体   繁体   中英

TypeScript generic type check changes behavior of the type when passed in enum

I encountered the following problem:

type NonUndefined<T> = T extends undefined ? never : T;

enum TestEnum {
    T1 = "T1",
    T2 = "T2",
}

interface Data {
    Val: TestEnum;
}

type AltFormData<T> = T extends object
    ? {
          readonly [P in keyof T]-?: AltFormData<NonUndefined<T[P]>>;
      } & AltFormValue<NonUndefined<T>>
    : AltFormValue<NonUndefined<T>>;

type Alt1FormData<T> = AltFormValue<NonUndefined<T>>;

type Alt2FormData<T> = T extends object ? AltFormValue<NonUndefined<T>> : AltFormValue<NonUndefined<T>>;

type AltFormValue<T = any> = {
    readonly $setValue: (value: T | undefined) => void;
};

const x2: AltFormData<Data> = {};
x2.Val.$setValue(TestEnum.T1); // ERROR - x2.Val is of type AltFormValue<TestEnum.T1> | AltFormValue<TestEnum.T2> and should be of type AltFormValue<TestEnum>

const x3: AltFormValue<TestEnum> = {};
x3.$setValue(TestEnum.T1); // GOOD

const x4: AltFormData<TestEnum> = {};
x4.$setValue(TestEnum.T1); // ERROR - x4 is of type AltFormValue<TestEnum.T1> | AltFormValue<TestEnum.T2> and should be of type AltFormValue<TestEnum>

const x5: AltFormValue<NonUndefined<TestEnum>> = {};
x5.$setValue(TestEnum.T1); // GOOD

const x6: Alt1FormData<TestEnum> = {};
x6.$setValue(TestEnum.T1); // GOOD

const x7: Alt2FormData<TestEnum> = {};
x7.$setValue(TestEnum.T1); // ERROR - x7 is of type AltFormValue<TestEnum.T1> | AltFormValue<TestEnum.T2> and should be of type AltFormValue<TestEnum>

It seems that type check T extends object is somehow changing the result. Same thing happends when using type "T1" | "T2" "T1" | "T2" instead of enum. You can copy and paste this code into the TS Playground or just click on the link .

It looks like typescript is treating the enum type as union of it's values really. This has consequences for conditional types.

So, when you have this type:

type Alt2FormData<T> = T extends object ? AltFormValue<NonUndefined<T>> : AltFormValue<NonUndefined<T>>;

And you will feed it the enum, which is really TestEnum.T1 | TestEnum.T2 TestEnum.T1 | TestEnum.T2 , typescript will actually compute the type.

AltFormValue<NonUndefined<TestEnum.T1>> | AltFormValue<NonUndefined<TestEnum.T2>>

This is call Distributive conditional types , the default how typescript is treating union types in conditionals. If you don't want that, you need to turn off this by wrapping T and object into brackets.

type Alt2FormData<T> = [T] extends [object] ? AltFormValue<NonUndefined<T>> : AltFormValue<NonUndefined<T>>;

This will result into this type to be computed instead:

AltFormValue<NonUndefined<TestEnum.T1 | TestEnum.T2>> 

If you simplify, then this is the desired type

AltFormValue<NonUndefined<TestEnum>> 

which is alias for:

{
    readonly $setValue: (value: TestEnum | undefined) => void;
}

Then you shouldn't see the compilation errors.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM