microsoft / TypeScript

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

NonNullablePartial type - Partial that doesn't allow explicit null/undefined values #34902

Open ackvf opened 4 years ago

ackvf commented 4 years ago

Search Terms

partial, nonnullable, required, undefined

Suggestion

This might seem as a duplicate of #13195, but this issue is different in that it doesn't require a possibly breaking change to the TS engine.

It instead gets advantage of a special generic Partial type that doesn't allow explicitly setting null/undefined.

// these of course sadly don't work

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

type NonNullablePartial<T> = {
  [P in keyof T]?: T[P] extends null | undefined | infer R ? R : T[P];
};

My suggestion adds type safety by guarding against what would likely be an error, which isn't yet possible with current implementation of Partial.

Use Cases

The reason for this issue is the fact that explicit null / undefined prevent the application of a "default" parameter with these patterns

/* In this example
 * default config = { message: 'message' }
 *    user config = { message: undefined }
 */

Object.assign({}, { message: 'message' }, { message: undefined }) // { message: undefined }

const obj = {  // obj = { message: undefined }
  ... { message: 'message' },
  ... { message: undefined }
}

This pattern does not suffer from that and an explicitly set undefined is reassigned with a default param. Though, this pattern is impractical for a large set of config variables.

function f({ message = 'message' } = {}) {
  console.log(message)
}

f({ message: undefined }); // message - correct!
f({ message: null });      // null  -  this makes sense, though not what I am looking for

To put it in other words: I am using a default config object, which I then need to merge with user config object. I need to prevent the user from possibly mistakenly assigning null/undefined values to parameters which would effectively replace and remove the "default" value.

Examples

export interface ResponseErrorTemplate {
  code: string;
  status: number;
  message: string;
  errorConstructor: typeof ApolloError;
}

export interface ResponseErrorTemplateInput {
  message: string;
  errorConstructor: typeof ApolloError;
}

export type ResponseErrorTemplateGetter = (
  config?: Partial<ResponseErrorTemplateInput> // Here, keep Partial, but prevent null/undefined
) => ResponseErrorTemplate;

const defaultConfig: ResponseErrorTemplateInput = {
  message: 'Not found',
  errorConstructor: ApolloError,
};

export const getNotFoundTemplate: ResponseErrorTemplateGetter = config => ({
  ...defaultConfig,
  ...config, // if user defines message: undefined here, they replace the default 'Not found'
  status: 404,
  code: 'NOT_FOUND',
});

Checklist

My suggestion meets these guidelines:

emilioplatzer commented 4 years ago

I need the same. You can see that the example at Typescript documentation is wrong:

https://www.typescriptlang.org/docs/handbook/utility-types.html#partialt

interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
};

const todo2 = updateTodo(todo1, {
    description: 'throw out trash',
});

todo2 is infered being Todo but generates Partial<Todo>

You can see at https://www.typescriptlang.org/play/index.html#code/JYOwLgpgTgZghgYwgAgCoHsAm7kG8CwAUMicmMGADYQBcyAzmFKAOYDcRpymE9CzAB3LoQdRsxDsiAXyJEYAVxAJhIZAoGY4kDNgAUYLOjq70AGmQxgESpnoYAqpu21kABThRycSgB5TAHwAlDSmBMSkUBBgClBquMgAdMmG2BbJiVY2do7OkMjSHISyhEQIIoxkRgCMyAC8eJyk5FSuAOToUCxwIMAAXig89ADWbWZNJEP8wELAInRtCNSeyEsKYJBQYzJFZRVgVdgATPXqeRCmBjUW4Vwt1HRKPFYgEJjjEZO807PzyG1gAAWUHQAHdkOh1mQoHB6IDtsUgrtCOUQPR0NREpR0CwrsckUA that if you pass the second parameter with undefined you obtain a Partial<Todo>:

interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>):Todo{
    return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
};

const todo2 = updateTodo(todo1, {
    title: undefined,
    description: 'throw out trash',
});

console.log(todo2);

This is what @ackvf says but using the example of Typescript documentation.

Use cases:

Having multilang app that allows to change some of ones,

setMessages(someMessages: PartialNotNull<Messages>) warning so you should not go undefined because those messages will not be displayed

ackvf commented 4 years ago

I have also updated my message to better explain the intention. In short: I need to prevent the user from making the properties undefined because they replace the config values from the default config object.

const obj: T = {  // obj = { message: undefined } - wrong
  ... { message: 'message' }, // default config object , type Partial<T>
  ... { message: undefined } // user config object, type Partial<T>, this shouldn't happen
}
qubitme commented 2 weeks ago

Are there any updates on this? A non-nullable / non-undefined Partial would be awesome!