Typescript: NonNullable isn't narrowing down object values' types for optional properties

Created on 6 Nov 2018  路  9Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.2.0-dev.20181106


Search Terms:
NonNullable object
NonNullable object values

Code

Run the following code via tsc --no-emit --strict test.ts:

interface P {
  color?: 'red' | 'green';
}

type RequiredP = {
  [K in keyof P]: NonNullable<P[K]>;
}

declare const p: RequiredP;
const color: 'red' | 'green' = p.color;

Expected behavior:

The resulting type should not allow undefined for the value at a property color.

Actual behavior:

undefined is still allowed. Using the NonNullable type doesn't seem to have any effect.

Playground Link: Note: you need to enable strictNullChecks manually!
https://www.typescriptlang.org/play/#src=interface%20P%20%7B%0D%0A%20%20color%3F%3A%20'red'%20%7C%20'green'%3B%0D%0A%7D%0D%0A%0D%0Atype%20RequiredP%20%3D%20%7B%0D%0A%20%20%5BK%20in%20keyof%20P%5D%3A%20NonNullable%3CP%5BK%5D%3E%3B%0D%0A%7D%0D%0A%0D%0Adeclare%20const%20p%3A%20RequiredP%3B%0D%0Aconst%20color%3A%20'red'%20%7C%20'green'%20%3D%20p.color%3B%0D%0A

Related Issues:

Mapped Types Experience Enhancement Suggestion

Most helpful comment

Here is the generic version:

type DeepNonNullable<T> = {
        [P in keyof T]-?: NonNullable<T[P]>;
}

All 9 comments

A workaround is to extract keyof P to a separate type first. It looks weird that it gives different result:

interface P {
  color?: 'red' | 'green';
}

type PKeys = keyof P;

type RequiredP = {
  [K in PKeys]: NonNullable<P[K]>;
}

declare const p: RequiredP;
const color: 'red' | 'green' = p.color;

https://www.typescriptlang.org/play/#src=interface%20P%20%7B%0D%0A%20%20color%3F%3A%20'red'%20%7C%20'green'%3B%0D%0A%7D%0D%0A%0D%0Atype%20PKeys%20%3D%20keyof%20P%3B%0D%0A%0D%0Atype%20RequiredP%20%3D%20%7B%0D%0A%20%20%5BK%20in%20PKeys%5D%3A%20NonNullable%3CP%5BK%5D%3E%3B%0D%0A%7D%0D%0A%0D%0Adeclare%20const%20p%3A%20RequiredP%3B%0D%0Aconst%20color%3A%20'red'%20%7C%20'green'%20%3D%20p.color%3B%0D%0A

The workaround from the previous comment doesn't work if the type is not known up front, e.g. in a function using generics. Example:

const makeRequired = <P extends {}>(props: P) => {
  type PKeys = keyof P;

  return props as {
    [Key in PKeys]: NonNullable<P[Key]>;
  };
};

interface Props {
  color?: 'red' | 'green';
}

declare const props: Props;

const enhancedProps = makeRequired(props);
const color: 'red' | 'green' = enhancedProps.color;

https://www.typescriptlang.org/play/#src=const%20makeRequired%20%3D%20%3CP%20extends%20%7B%7D%3E(props%3A%20P)%20%3D%3E%20%7B%0D%0A%20%20type%20PKeys%20%3D%20keyof%20P%3B%0D%0A%0D%0A%20%20return%20props%20as%20%7B%0D%0A%20%20%20%20%5BKey%20in%20PKeys%5D%3A%20NonNullable%3CP%5BKey%5D%3E%3B%0D%0A%20%20%7D%3B%0D%0A%7D%3B%0D%0A%0D%0Ainterface%20Props%20%7B%0D%0A%20%20color%3F%3A%20'red'%20%7C%20'green'%3B%0D%0A%7D%0D%0A%0D%0Adeclare%20const%20props%3A%20Props%3B%0D%0A%0D%0Aconst%20enhancedProps%20%3D%20makeRequired(props)%3B%0D%0Aconst%20color%3A%20'red'%20%7C%20'green'%20%3D%20enhancedProps.color%3B%0D%0A

using the syntax { key: type | undefined } instead of the optional property syntax using { key?: type } makes the construct work on typescript 3.5.1.

This is a pretty glaring issue; it's a rather basic piece of functionality that simply doesn't work.

@mgol Let me know if I misunderstood the problem, but I think this issue is just about the fact that your RequiredP is a homomorphic mapped type. This means that the optionality of the field is preserved in RequiredP If you use -? (added by this PR) you can remove the optional modifier explicitly and all will work as expected as far as I can tell:

interface P {
  color?: 'red' | 'green';
}

type RequiredP = {
  [K in keyof P]-?: NonNullable<P[K]>;
}

declare const p: RequiredP;
const color: 'red' | 'green' = p.color; // no error, previously an error because p.color still contained undefined

play

Note: Mapped types are only homomorphic if they map over keyof T where T is any type or if they map over K where K is a type parameter extending keyof T. This is why your workarounds worked because they broke the pattern for homomorphic mapped types.

@dragomirtitian thanks very much for that incredibly informative comment. I had never seen the -? syntax before, but that is definitely what's missing in this situation. I've not encountered any docs for it before, but it does seem like a very niche/special-case feature built especially for this situation.

Thanks again for taking the time to explain that so well!

Here is the generic version:

type DeepNonNullable<T> = {
        [P in keyof T]-?: NonNullable<T[P]>;
}

There's already a type for this in lib.es5.d.ts:

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

Not sure when it got added, but our long wait is over.
Think it's safe to close this issue?

edit: Damn, thought Required would solve all our issues and bring about world peace.

Required<T> allows the type null by default. It also allows the type undefined if you don't use questionmark on the field (Look at the example 'var r1 = ...'). So it does not provide the functionality we might expect from a NonNullable type. I think DeepNonNullable<T> implementation in the example below gives the expected results on object-and field-level;

Click for playground.

type DeepNonNullable<T> =
  { [P in keyof T]-?: NonNullable<T[P]>; } & NonNullable<T>
//wiht '?'
interface A {
  color?: 'red' | 'green' | undefined | null;
}
//wihtout '?'
interface B {
  color: 'red' | 'green' | undefined | null;
}
//wiht '?'
interface C {
  color?: DeepNonNullable<'red' | 'green' | undefined | null>
}
//wihtout '?'
interface D {
  color: DeepNonNullable<'red' | 'green' | undefined | null>
}
//wiht '?'
interface E {
  color?: NonNullable<'red' | 'green' | undefined | null>
}

//wihtout '?'
interface F {
  color: NonNullable<'red' | 'green' | undefined | null>
}

//wiht '?'
interface G {
  color?: Required<'red' | 'green' | undefined | null>
}

//wihtout '?'
interface H {
  color: Required<'red' | 'green' | undefined | null>
}

// first letter: type indicator
// d: object deepnonnullable 
// r: object level required
//_d: field level deepnonnullable
//_n: field level nonnullable
//_r: field level required

// q: (as second letter) field wiht questionmark ({color?:...})

var dq0: DeepNonNullable<A> = { color: 'green' }  //OK
var dq2: DeepNonNullable<A> = { color: null }     //Type 'null' is not assignable
var dq1: DeepNonNullable<A> = { color: undefined }//Type 'undefined' is not assignable

var d0: DeepNonNullable<B> = { color: 'green' }  //OK
var d2: DeepNonNullable<B> = { color: null }     //Type 'null' is not assignable
var d1: DeepNonNullable<B> = { color: undefined }//Type 'undefined' is not assignable

var rq0: Required<A> = { color: 'green' }  //OK
var rq2: Required<A> = { color: null }     //OK
var rq1: Required<A> = { color: undefined }//Type 'undefined' is not assignable

var r0: Required<B> = { color: 'green' }  //OK
var r2: Required<B> = { color: null }     //OK
var r1: Required<B> = { color: undefined }//OK

var _dq0: C = { color: 'green' }           //OK
var _dq2: C = { color: null }              //Type 'null' is not assignable
var _dq1: C = { color: undefined }         //OK

var _d0: D = { color: 'green' }           //OK
var _d2: D = { color: null }              //Type 'null' is not assignable
var _d1: D = { color: undefined }         //Type 'undefined' is not assignable

var _nq0: E = { color: 'green' }           //OK
var _nq2: E = { color: null }              //Type 'null' is not assignable
var _nq1: E = { color: undefined }         //OK

var _n0: F = { color: 'green' }           //OK
var _n2: F = { color: null }              //Type 'null' is not assignable
var _n1: F = { color: undefined }         //Type 'undefined' is not assignable

var _rq0: G = { color: 'green' }           //OK
var _rq2: G = { color: null }              //OK
var _rq1: G = { color: undefined }         //OK

var _r0: H = { color: 'green' }           //OK
var _r2: H = { color: null }              //OK
var _r1: H = { color: undefined }         //OK

Was this page helpful?
0 / 5 - 0 ratings