microsoft / TypeScript

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

Allow mapped tuples to have behaviour based on index #29920

Open Roaders opened 5 years ago

Roaders commented 5 years ago

Search Terms

mapped tuples index

Suggestion

I want to be able to create a tuple from function parameters and have all elements in the tuple optional other than the firstK

Use Cases

I am trying to create a curry implementation that allows you to curry any number of parameters. The first parameter must be specified but all other params are optional. Would be good to be able to remove the last param as well.

Examples

something along the lines of

type OptionalExceptFirst<T> = {
    [K in keyof T]:isFirst ? ? T[K] : T[K];
};
type AllExceptLast<T> = {
    [K in keyof T]:isLast ? never : T[K];
};

The syntax will need some work hopefully the intended functionality is clear.

Checklist

My suggestion meets these guidelines:

Nathan-Fenner commented 5 years ago

You can do this today in TypeScript:

// make a tuple into a tuple whose elements are **all** optional:
type AllOptional<T extends unknown[]> = { [k in keyof T]?: T[k] }

type RestOptional<T extends unknown[]> =
    ((...xs: T) => unknown) extends ((h: infer Head, ...ts: infer Tail) => any)
        ?  ((h: Head, ...ts: AllOptional<Tail> ) => void) extends (...a: infer A) => any ? A : never
        : never;

You can use it like this:

type Example = RestOptional<[number, string, number>];

// type Example = [number, (string | undefined)?, (number | undefined)?]
Roaders commented 5 years ago

That's great, thank you very much.

To make it a bit easier to understand (for me!) I refactored it a bit:

type AllOptional<T extends unknown[]> = { [k in keyof T]?: T[k] }

type CombineHeadTail<Head, Tail extends unknown[]> = ((h: Head, ...ts: AllOptional<Tail> ) => void) extends (...a: infer A) => any ? A : never

type TailOptional<T extends unknown[]> = 
    ((...allParams: T) => unknown) extends ((head: infer Head, ...tail: infer Tail) => any) ?
    CombineHeadTail<Head, Tail> : never;

I still can't understand why this works though. ((...allParams: T) => unknown) extends ((head: infer Head, ...tail: infer Tail) => any) implies that T has to be function params but in your example it obviously works when used on any tuple.

Roaders commented 5 years ago

I realised after I posted this issue that the actual difficult bit about what I am trying to achieve is the return type.

If I have this function:

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

then I want my curry function to have the following arguments (this I can now achieve):

curry(myFunction, one: string, two?: number, three?: boolean)

but I want the return type of this function to be based on the number of arguments filled in:

curry(myFunction, one: string) => (two: number, three: boolean) => ReturnType;
curry(myFunction, one: string, two: number) => (three: boolean) => ReturnType;
curry(myFunction, one: string, two: number, three: boolean) => () => ReturnType;

this is easy to achieve with function overrides but not in a generic, mapped (magic) way!

Thanks for your help