TypeScript Version: 2.9.0-dev.20180405
Search Terms: "extends never"
Code
I am writing a function for testing conditional types and I'd like to write it like this:
function assert<T extends true>(expectTrue: T extends never ? false : true) {
}
// or even
function assert<T extends true>(expectTrue: T extends true ? true : false) {
}
That way I can write:
type IsNullableType<T> = T extends null | undefined ? true : never;
const result = functionUnderTest(someParam);
assert<IsNullableType<typeof result>>(true);
// or
assert<IsNullableType<typeof result>>(false);
So given this:
assert<IsNullableType<string>>(false);
Expected behavior:
false
.Actual behavior:
never
.false
is not assignable to never
.Example where extends never
works:
type IsNonNullableType<T> = IsNullableType<T> extends never ? true : never;
assert<IsNonNullableType<string>>(true); // no compile error, works fine
// this doesn't cause a compile error in playground, but it correctly throws a compile error in the latest version
assert<IsNonNullableType<string | undefined>>(true); // compile error, as expected
Playground Link: Link.
Basically, an IsNeverType
check does not work: type IsNeverType<T> = T extends never ? true : never;
Edit: By the way, this is probably a better way to do my check:
export type IsNullableType<T> = Extract<T, null | undefined> extends never ? false : true;
I'm just thinking this might be expected behaviour. So when a type is passed never
(as T
) and there's an expression like T extends ... ? ... : ...
, then it will resolve that entire T extends ... ? ... : ...
to never
?
Anyway, I resolved my issue by making everything work with true
or false
, which is what I probably should have been doing from the start anyway... just couldn't figure out the right way to do it, but Extract
helped.
IsNeverType
needs to be implemented like:
type IsNeverType<T> = [T] extends [never] ? true : never;
to stop it being distributive.
Seems like a bug to me?
// Actual instantiation of 'T' shouldn't matter - conditional returns 'number' either way
function assert<T>(expectTrue: T extends never ? number : number) { }
// OK
assert<boolean>(0);
assert<string>(0);
assert<true>(0);
// Error; argument of type '0' not assignable to 'never'
assert<never>(0);
As far as my understanding goes the conditional doesn't always return number
, it can also return never
because it may distribute over nothing.
// Ensure conditional is always `number`.
function assert<T>(expectTrue: [T] extends [never] ? number : number) { }
// OK
assert<boolean>(0);
assert<string>(0);
assert<true>(0);
assert<never>(0);
After reading the code I agree with @jack-williams 's assessment. In this case never
behaves as the empty union, so it distributes over the conditional and produces another empty union (which is just never
again)
@RyanCavanaugh good to know! Thanks for looking into this and explaining the behaviour.
@jack-williams thanks a lot for that trick! Very helpful! 馃槃
Holy cow, this just bit me. The empty union!
I've created simple helper type
type NeverAlternative<T, P, N> = [T] extends [never] ? P : N;
// and then
type VariableLengthArray<A, B = never, C = never> =
//
NeverAlternative<
C,
NeverAlternative<B, [A], [A, B]>,
NeverAlternative<
B,
[A, C],
[A, B, C]
>
>;
type Foo = VariableLengthArray<1,2> // type [1, 2]
type Bar = VariableLengthArray<1,2,3> // type [1, 2, 3]
Most helpful comment
IsNeverType
needs to be implemented like:to stop it being distributive.