Open mgol opened 5 years ago
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;
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;
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 null
by default. It also allows undefined
if you don't use ?
(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-levels;
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
You can use required-properties
package for this.
import { assertRequiredProperties } from "required-properties";
interface P {
color: "red" | "green" | undefined;
stillCanBeNullable: number | undefined | null;
}
declare const p: P;
assertRequiredProperties(p, ["color"]); // full autocomplete here
const color: "red" | "green" = p.color; // no error - color is required β
Benefits: π‘
p
gets infered automaticallyTs playground here
Note this doesn't work with optional property in your description. However you can solve it by defining it as | undefined
Here is the generic version:
type DeepNonNullable<T> = { [P in keyof T]-?: NonNullable<T[P]>; }
Here is a type guard version.
export const hasNoDeepNullable = <T extends Record<string, unknown>>(
obj: T,
): obj is DeepNonNullable<T> => {
for (const key of Object.keys(obj)) {
if (obj[key] === null) return false;
}
return true;
};
If all of the nullable values exist, it simply applies the DeepNonNullable<>
type to the object.
To specify the keys for null checks:
export const valuesAreNotNullable = <T extends Record<string, unknown>, K extends Array<keyof T>>(
obj: T,
keys: K,
): obj is Omit<T, K[number]> & Pick<DeepNonNullable<T>, K[number]> => {
for (const key of keys) {
if (obj[key] === null) return false;
}
return true;
};
Example usage will be:
if (!valuesAreNotNullable(user, ['detail'])) error(500);
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
:Expected behavior:
The resulting type should not allow
undefined
for the value at a propertycolor
.Actual behavior:
undefined
is still allowed. Using theNonNullable
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%0ARelated Issues: