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:
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
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
Most helpful comment
Here is the generic version: