Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript Partial<T> type without undefined

Tags:

How to create a kinda-Partial<T> type, that doesn't allow undefined values?

Here's an example:

interface MyType {   foo: string   bar?: number }  const merge = (value1: MyType, value2: KindaPartial<MyType>): MyType => {   return {...value1, ...value2}; }    const value = {   foo: 'foo',   bar: 42 }  merge(value, {});                          // should work merge(value, { foo: 'bar' });              // should work merge(value, { bar: undefined });          // should work merge(value, { bar: 666 });                // should work merge(value, { foo: '', bar: undefined }); // should work merge(value, { foo: '', bar: 666 });       // should work  // now the problematic case: merge(value, { foo: undefined }); // this should throw an error                                   // because MyType["foo"] is of type string 

The type I'm looking for should:

  • only accept keys that exist on the generic type (just like a normal Partial<T>)
  • accept a subset of the keys of the generic type
  • but don't accept undefined if the generic type doesn't accept undefined for that key

Is this possible?


EDIT: I also created a issue at TypeScript repository, because this is weird and should throw an error at some point: https://github.com/Microsoft/TypeScript/issues/29701

like image 215
Benjamin M Avatar asked Feb 02 '19 03:02

Benjamin M


2 Answers

TS 4.4 UPDATE:

TS4.4 will have an --exactOptionalPropertyTypes compiler flag to give you the behavior you’re looking for directly with Partial, as long as you intentionally add undefined where you'd like to allow it:

interface MyType {   foo: string   bar?: number | undefined // <-- you want this }  const merge = (value1: MyType, value2: Partial<MyType>): MyType => {   return { ...value1, ...value2 }; }   const value = {   foo: 'foo',   bar: 42 }  merge(value, {});                          // okay merge(value, { foo: 'bar' });              // okay merge(value, { bar: undefined });          // okay merge(value, { bar: 666 });                // okay merge(value, { foo: '', bar: undefined }); // okay merge(value, { foo: '', bar: 666 });       // okay  // now the problematic case: merge(value, { foo: undefined }); // error! // ----------> ~~~ // Type 'undefined' is not assignable to type 'string' 

Playground link to code <-- note, currently you will need to turn on --exactOptionalPropertyTypes yourself in the TS Config tab; for some reason the url is broken

β€”β€”--

PRE-TS4.4 ANSWER:

It is a known limitation (see microsoft/TypeScript#13195) that TypeScript doesn't properly distinguish between object properties (and function parameters) which are missing from ones with are present but undefined. The fact that Partial<T> allows undefined properties is a consequence of that. The right thing to do is to wait until this issue is addressed (this might become more likely if you go to that issue in GitHub and give it a πŸ‘ or a comment with a compelling use case).

If you don't want to wait, you can maybe use the following hacky way to get something like this behavior:

type VerifyKindaPartial<T, KP> =    Partial<T> & {[K in keyof KP]-?: K extends keyof T ? T[K] : never};   const merge = <KP>(value1: MyType, value2: KP & VerifyKindaPartial<MyType, KP>): MyType => {   return { ...value1, ...value2 }; } 

So you can't write KindaPartial<T> directly. But you can write a type VerifyKindaPartial<T, KP> that takes a type T and a candidate type KP that you want to check against your intended KindaPartial<T>. If the candidate matches, then it returns something that matches KP. Otherwise it returns something that does not.

Then, you make merge() a generic function that infers KP from the type of the value passed into value2. If KP & VerifyKindaPartial<MyType, KP> matches KP (meaning that KP matches KindaPartial<MyType>), then the code will compile. Otherwise, if KP & VerifyKindaPartial<MyType, KP> does not match KP (meaning that KP does not match KindaPartial<MyType>), then there will be an error. (The error might not be very intuitive, though).

Let's see:

merge(value, {});                          // works merge(value, { foo: 'bar' });              // works merge(value, { bar: undefined });          // works merge(value, { bar: 666 });                // works merge(value, { foo: '', bar: undefined }); // works merge(value, { foo: '', bar: 666 });       // works  merge(value, { foo: undefined }); // error! //             ~~~ <-- undefined is not assignable to never //  the expected type comes from property 'foo',  

That has the behavior you want... although the error you get is a bit weird (ideally it would say that undefined is not assignable to string, but the problem is that the compiler knows the passed-in type is undefined, and it wants the type to be string, so the compiler intersects these to undefined & string which is never. Oh well.

Anyway there are probably caveats here; generic functions work well when called directly but they don't compose well because TypeScript's support of higher-kinded types isn't that good. I don't know if this will actually work for your use case, but it's the best I can do with the language as it currently is.

Playground link to code

like image 161
jcalz Avatar answered Sep 22 '22 19:09

jcalz


In this case, Pick should work.

interface MyType {   foo: string   bar?: number }  const merge = <K extends keyof MyType>(value1: MyType, value2: Pick<MyType, K>): MyType => {   return {...value1, ...value2}; }  merge(value, {});                          // ok merge(value, { foo: 'bar' });              // ok merge(value, { bar: undefined });          // ok merge(value, { bar: 666 });                // ok merge(value, { foo: '', bar: undefined }); // ok merge(value, { foo: '', bar: 666 });       // ok  merge(value, { foo: undefined });          // ng 

Playground link

like image 21
kimamula Avatar answered Sep 22 '22 19:09

kimamula