Typescript: Conditional Types - Checking `extends never` only works sometimes

Created on 5 Apr 2018  路  8Comments  路  Source: microsoft/TypeScript

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:

  • The type of the parameter resolves to false.
  • No error.

Actual behavior:

  • The type of the parameter resolves to 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;
Working as Intended

Most helpful comment

IsNeverType needs to be implemented like:

type IsNeverType<T> = [T] extends [never] ? true : never;

to stop it being distributive.

All 8 comments

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]
Was this page helpful?
0 / 5 - 0 ratings