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

optional and readonly Filter Modifier for keyof #35103

Open 1nVitr0 opened 4 years ago

1nVitr0 commented 4 years ago

Search Terms

Suggestion

Adding Support for modifiers to keyof could be very helpful. I'm picturing something like this

type Koptional = keyof? T;
type Kreadonly = readonly keyof T;

// Negations
type Krequired = keyof!? T;
type Keditable = !readonly keyof T;

// Combinations
type KrequiredReadonly = readonly keyof!? T;

The modifiers would act as "filters", only selecting the keys fitting the given modifiers.


As @jcalz pointed out, it might make more sense to use the existing + and - operators instead of the !.

Use Cases

This could be really useful for selecting properties of types based on them being optional / readonly. A possible use case for this would be the React defaultProps object (see example).

Of course this could negatively affect readability as especially Mapped Types can get fairly long

type PickEditableRequired<T> = {
    [P in !readonly keyof!? T]: T[P];
};

but I think it's still in the realms of comprehensibility.

Examples

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

type DrinkProps = {
    whoGetsUpAndMakesIt: string,
    type?: "water" | "coffee" | "coke" | "more coffee",
    vessel?: "cup" | "glass" | "HUGE cup",
    amount?: number
};

class Drink extends React.Component<Required<DrinkProps>> {
    public static defaultProps: PickOptional<DrinkProps> = {
        type: "water",
        vessel: "glass",
        amount: 250
    }

    render() {
        return <p>
            {this.props.whoGetsUpAndMakesIt} gets up and pours
            {this.props.amount}ml of {this.props.type} into a
            {this.props.vessel}.
        </p>
    }
}

Especially with many optional props the current method of

public static defaultProps: Pick<DrinkProps, "type" | "vessel" | "amount"> = { ... };

can get fairly tedious, especially as you have to add new optional props at 3 places (the type definition, the Pick UtilityType and in the defaultProps). The method using PickOptional and the ?-keyof-modifier reduces it to 2, and immediately notifies you if you declared a parameter optional, but haven't defined it in defaultProps.

This would also work without React in terms of default values:

type Order = {
    item: string,
    amount?: number,
    shipping?: "standard" | "express"
}
const defaultOrder: PickOptional<Order> = {
    amount: 1,
    shipping: "standard"
}

function placeOrder(order: Order): void {
    let data: Required<Order> = {...defaultOrder, ...order};
    console.log(`You ordered ${data.amount} ${data.item} with ${data.shipping} shipping.`);
}

I'm sure there are more (and better) use cases for this feature I can't think of right now...

Checklist

My suggestion meets these guidelines:

jcalz commented 4 years ago

Related to #31581

Relevant SO question.

Bikeshedding: I think ! in the type system is generally used to mean "definitely defined" and not "not". Instead you'd probably want to reuse the + and - modifier modifiers as introduced in TS2.8.

1nVitr0 commented 4 years ago

Relevant SO question.

Thanks @jcalz , somehow I missed that So topic when I googled about this. The '+' / '-' makes sense, in my mind they were more like "add that modifier" and "remove that modifier" than "does it have that modifier?". Will add that to my question.

charles-allen commented 4 years ago

[1] Here's a relevant SO answer for Optional/Non-Optional filtering.

[2] I think there's one more combination:

type KrequiredEditable = !readonly keyof!? T;

Edit: Silly me; of course 2x2 => 4 combinations:

type KOptionalWritable = !readonly keyof? T;
type KRequiredWritable = !readonly keyof!? T;
type KOptionalReadonly = readonly keyof? T;
type KRequiredReadonly = readonly keyof!? T;

[3] With respect to +/-, they do mean add/remove. I think it may be confused with this existing usage (which means "make writable" not "is writable?"):

export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
charles-allen commented 4 years ago

An alternative syntax proposal

Since ! is already used as the non-null assertion operator (and grammatically it feels opposite to ?), I think using it here provides a clean syntax to filter for non-null/non-undefined (required) keys:

type KOptional = keyof? T; // only optional keys
type KRequired = keyof! T; // only required keys

A more explicit opposite of readonly is "writable":

type KReadonly = readonly keyof T; // only readonly keys
type KWritable = writable keyof T; // only writable keys

I find this much more readable, especially when you consider combinations:

type KOptionalWritable = writable keyof? T;
type KRequiredWritable = writable keyof! T;
type KOptionalReadonly = readonly keyof? T;
type KRequiredReadonly = readonly keyof! T;
joshxyzhimself commented 2 years ago

Looking for this,

anthonyborell commented 5 months ago

This would be really useful