Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow conditionally setting optional properties in a mapped type #36126

Open
5 tasks done
Roaders opened this issue Jan 10, 2020 · 7 comments
Open
5 tasks done

Allow conditionally setting optional properties in a mapped type #36126

Roaders opened this issue Jan 10, 2020 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@Roaders
Copy link

Roaders commented Jan 10, 2020

Search Terms

conditional optional mapped type

Suggestion

Allow us to conditionally apply ? to a property in a mapped type

Use Cases

I have a function:

function myFunction(one: string, two: number, three: number){}

but I want to map the params to

function myFunction(one: string, two?: number, three?: number){}

Examples

Suggested approach:

type MapParams<T> = {
    [P in keyof T]: T[P] extends string ? T[P] : | ?T[P]
}

(or something similar)

This is the closest I could come:

type MapParams<T> = {
    [P in keyof T]: T[P] extends string ? T[P] : | T[P] | undefined
}

but that produces

function myFunction(one: string, two: number | undefined, three: number | undefined){}

myFunction("", undefined, undefined)

Playground

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@Roaders
Copy link
Author

Roaders commented Jan 10, 2020

From @DanielRosenwasser on Twitter:

You could imagine an intersection of two mapped types - one with optional properties and one with required. I think that might be what you had in mind?

So from this I imagine a solution like this:

type Optional<T> = {
    [P in keyof T]?: T[P]
}

type MapParams<T> = {
    [P in keyof T]: T extends string ? T[P] : Optional<T>[P]
}

but this still doesn't work. If I do this:

type Optional<T> = {
    [P in keyof T]?: T[P]
}

type MapParams<T> = {
    [P in keyof T]: Optional<T>[P];
}

then the ? in Optional gets converted into T[P] | undefined in MapParams

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jan 10, 2020
@jcalz
Copy link
Contributor

jcalz commented Jan 13, 2020

Duplicate of #32562

@jorroll
Copy link

jorroll commented Nov 23, 2020

I also ran into this limitation. As pointed out #32562, it is possible to workaround this limitation (I'm posting here because I think this issue has a much clearer title/description and is easier to find).

Here is an ugly but working solution that will hopefully help someone else:

type PickUndefinedKeys<T> = {
  [K in keyof T]: undefined extends T[K] ? K : never;
}[keyof T];

type PickRequiredKeys<T> = {
  [K in keyof T]: undefined extends T[K] ? never : K;
}[keyof T];

type SelectOptional<T extends {}> = Pick<
  T,
  Exclude<PickUndefinedKeys<T>, undefined>
>;

type SelectRequired<T extends {}> = Pick<
  T,
  Exclude<PickRequiredKeys<T>, undefined>
>;

// EXAMPLES

type Test1 = SelectOptional<{one?: string; two: number}>;
// => { one?: string }

type Test2 = SelectRequired<{one?: string; two: number}>
// => { two: number }

As a more complex example, say you want to map the types in an object while retaining each property's original optional status:

interface IPerson {
  name: string
}

type OptionalNameMap<
  T extends { [key: string]: IPerson | undefined }
> = {
  [P in Exclude<PickUndefinedKeys<T>, undefined>]?: NonNullable<T[P]>['name'];
};

type RequiredNameMap<
  T extends { [key: string]: IPerson | undefined }
> = {
  [P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>['name'];
};

type MapNames<T extends { [key: string]: IPerson | undefined }> = RequiredNameMap<T> & OptionalNameMap<T>;

// EXAMPLE

type NameDictionary = MapNames<{one: IPerson, two?: IPerson }>;
// => { one: string, two?: string | undefined }

Edit:
It's worth noting that I don't think this workaround can differentiate between { one?: string } and { one: string | undefined }. In my case, that's fine, but that might be problematic for others.

@DavideValdo
Copy link

DavideValdo commented May 17, 2021

I managed to pull this off with some work:

type TestType = {
  a: SomeInterface;
  b: string;
};

type Intersection<A, B> = A & B extends infer U
  ? { [P in keyof U]: U[P] }
  : never;

type Matching<T, SomeInterface> = {
  [K in keyof T]: T[K] extends SomeInterface ? K : never;
}[keyof T];

type NonMatching<T, SomeInterface> = {
  [K in keyof T]: T[K] extends SomeInterface ? never : K;
}[keyof T];

type DesiredOutcome = Intersection<
  Partial<Pick<TestType, Matching<TestType, SomeInterface>>>,
  Required<Pick<TestType, NonMatching<TestType, SomeInterface>>
>

I think it would be confusing to have the logic for adding ? in the right side of a property declaration

It would be awesome to have this syntax:

type Optionalize<T> = {
   [P in keyof T](?: T[P] extends SomeInterface): ...;
} 

But I guess this is not such a common problem to justify the effort.

@OoDeLally
Copy link

I was thinking about this, and I have a syntax proposal that doesn't require any parser change:

type MyKeys = 'optionalKey' | 'requiredKey';
type Foo = {
   [Key in keyof MyKeys as Key extends 'optionalKey' ? Key | undefined : Key]: string;
}
// Result is
{
  optionalKey?: string;
  requiredKey: string;
}

Note that TS already uses such "trick" with never to command not to add the key at all, so I think my proposal is the closest to an existing behavior.

@ecyrbe
Copy link

ecyrbe commented Jul 31, 2023

Here is a proposal syntax leveraging on K in keyof T as syntax but allowing modifiers to be part of brakets :

type MyKeys = 'optionalKey' | 'requiredKey' | 'readonlyKey' | 'readonlyOptionalKey';
type Foo = {
   [Key in keyof MyKeys as Key extends 'optionalKey' ? 
                                             Key+? : 
                                           Key extends 'readonlyKey' ? 
                                             Key+readonly :
                                           Key extends 'readonlyOptionalKey ?
                                             Key+?+readonly :
                                           Key
   ]: string;
}
// Result is
{
  optionalKey?: string;
  requiredKey: string;
  readonly readonlyKey: string;
  readonly readonlyOptionalKey?: string;
}

if you use '-' instead of '+' it removes the modifier :

type MyKeys = 'optionalKey' | 'requiredKey' | 'readonlyKey' | 'readonlyOptionalKey';
type Foo = {
    readonly [Key in keyof MyKeys as Key extends 'requiredKey' ? 
                                             Key-?-readonly : 
                                           Key extends 'readonlyKey' ? 
                                             Key-? :
                                           Key extends 'optionalKey' ?
                                             Key-readonly :
                                           Key
   ]?: string;
}
// Result is
{
  optionalKey?: string;
  requiredKey: string;
  readonly readonlyKey: string;
  readonly readonlyOptionalKey?: string;
}

@microsoft microsoft deleted a comment from mbohgard Mar 21, 2024
@layerok
Copy link

layerok commented Jun 22, 2024

By combing PickOptinalProperties and PickRequiredProperties types, I was able to construct a type that makes properties that extend undefined optional

export type MakeUndefinedPropertiesOptional<T> = Partial<
  Pick<T, PickUndefinedKeys<T>>
> &
  Pick<T, PickRequiredKeys<T>>;
  
type Props = {
   params: undefined;
   data: undefined;
}

const props: MakeUndefinedPropertiesOptional<Props> = {}; // no error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants