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

Improved control over mapped type modifiers #21919

Merged
merged 5 commits into from
Feb 13, 2018
Merged

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Feb 13, 2018

Mapped types currently support adding a readonly or ? modifier to a mapped property, but they do not provide support the ability to remove modifiers. This matters in homomorphic mapped types (see #12563 and #12826) which by default preserve the modifiers of the underlying type.

With this PR we provide the ability for a mapped type to either add or remove a particular modifier. Specifically, a readonly or ? property modifier in a mapped type can now be prefixed with either + or - to indicate that the modifier should be added or removed.

Using this ability, the PR defines a new Required<T> type in lib.d.ts. This type strips ? modifiers from all properties of T, thus making all properties required:

type Required<T> = { [P in keyof T]-?: T[P] };

Some examples of + and - on mapped type modifiers:

type MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] };  // Remove readonly and ?
type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] };  // Add readonly and ?

A modifier with no + or - prefix is the same as a modifier with a + prefix. So, the ReadonlyPartial<T> type above corresponds to

type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] };  // Add readonly and ?

In --strictNullChecks mode, when a homomorphic mapped type removes a ? modifier from a property in the underlying type it also removes undefined from the type of that property:

type Foo = { a?: string };  // Same as { a?: string | undefined }
type Bar = Required<Foo>;  // Same as { a: string }

Fixes #15012.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 13, 2018

We should talk about this in the next design meeting though.

@ahejlsberg ahejlsberg merged commit f8a378a into master Feb 13, 2018
@ahejlsberg ahejlsberg deleted the mappedTypeModifiers branch February 13, 2018 19:21
@yortus
Copy link
Contributor

yortus commented Feb 14, 2018

Is there any way filter out just the required properties or just the optional properties?

I've been trying to make a type mapping that does this with no luck so far.

@ahejlsberg
Copy link
Member Author

@yortus Something like this:

type OptionalPropNames<T> = { [P in keyof T]: undefined extends T[P] ? P : never }[keyof T];
type RequiredPropNames<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];

type OptionalProps<T> = { [P in OptionalPropNames<T>]: T[P] };
type RequiredProps<T> = { [P in RequiredPropNames<T>]: T[P] };

type Foo = {
    a: string;
    b: number;
    c?: string;
    d?: number;
}

type T1 = RequiredProps<Foo>;  // { a: string, b: number }
type T2 = OptionalProps<Foo>;  // { c?: string | undefined, d?: number | undefined }

Strictly speaking this doesn't filter on optional vs. required, but rather whether undefined is assignable to the property or not. But given that required properties rarely if ever have undefined as a permitted value, it's largely the same.

@zpdDG4gta8XKpMCd
Copy link

i salute the idea, but somehow this syntax doesn't seem scalable

@yortus
Copy link
Contributor

yortus commented Feb 14, 2018

@ahejlsberg thanks, that's just what I need.

@Aleksey-Bykov the syntax makes me uncomfortable too. TypeScript is steadily building up really useful full compile-time programming capabilities, but some of the syntax seems very specific to a few cases (e.g. this PR). Having a few highly orthogonal compile-time programming constructs that also covered these special cases would be ideal. But I'm sure that's much easier said than done.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Feb 14, 2018

on a constructive note, at some point when there are over 10 different modifiers at play we gonna need some meta syntax to control them, something like this, forgive my french:

{
     [{ // <--- sort of attribute syntax of C#?
           amIReadonly: true || shouldIBeSomethingElse,
           canBeUndefined: canBePossiblyUndefined && thereIsNoHope
     }]
     value: number
}

or something like Object.defineProperty

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Feb 14, 2018

to extend this idea to mapped or nested types by making a set modifiers a function of the previous/higher set of modifiers

{
     [before => ({
           isReadonly: !before.isReadonly,
           canBeUndefined: true
     })]
     value: number
}
@krryan
Copy link

krryan commented Feb 14, 2018

These look good to me, and I'm actually pretty OK with the syntax. If something better were suggested, sure, but I'd be comfortable/happy with using this one.

As for @Aleksey-Bykov suggestions, compile-time, type-domain functions in general would be a really useful thing to have. Conditional types get us a big step closer to that, but we still aren't there. This might be a nice place to start.

@KiaraGrouwstra
Copy link
Contributor

I think we're making big strides forward. With the pattern matching and recursion, I got a bunch of types to work that just hadn't been possible before (e.g. setting/removing readonly/? recursively in nested structures, flattening promises/arrays).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
6 participants