microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.05k stars 12.37k forks source link

NonNullable isn't narrowing down object values' types for optional properties #28374

Open mgol opened 5 years ago

mgol commented 5 years ago

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:

mgol commented 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;

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

mgol commented 5 years ago

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

medfreeman commented 5 years ago

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

Ameobea commented 4 years ago

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

dragomirtitian commented 4 years ago

@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.

Ameobea commented 4 years ago

@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!

hiyelbaz commented 4 years ago

Here is the generic version:

type DeepNonNullable<T> = {
        [P in keyof T]-?: NonNullable<T[P]>;
}
larssn commented 4 years ago

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.

hiyelbaz commented 4 years ago

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
dominikjasek commented 1 year ago

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: πŸ’‘

  1. You can select which properties are required (not all of them)
  2. This package throws error at runtime if property is nullable
  3. No need to declare new type - type of p gets infered automatically

Ts playground here

Note this doesn't work with optional property in your description. However you can solve it by defining it as | undefined

hyunbinseo commented 7 months ago

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);