-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Mapped conditional types #12424
Comments
Surely it should be implemented. However, need to find an appropriate syntax. There is possible clash with other proposal: #4890 |
Another possible use case is typeguarding Knockout.js mappings, which needs choosing between In interface Item {
id: number;
name: string;
subitems: string[];
}
type KnockedOut<T> = T extends Array<U> ? KnockoutObservableArray<U> : KnockoutObservable<T>;
type KnockedOutObj<T> = {
[P in keyof Item]: KnockedOut<Item[P]>;
}
type KoItem = KnockedOutObj<Item>
type KoItem = {
id: KnockoutObservable<number>;
name: KnockoutObservable<string>;
subitems: KnockoutObservableArray<string>;
} |
Hi, I was just reading "GADTs for dummies" (which might be helpful for anyone interested in this issue) where GADT = "Generalized Algebraic Data Type". Although I'm not quite really there in getting a full understanding of the concept, it did occur to me that what is described here can alternatively be elegantly expressed through a form of "overloading", or more specifically, pattern matching, over type constructors: type Primitive = string | number | boolean | undefined | null;
type DeepReadonly<T extends Primitive> = T;
type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]>; }; The idea is that this works just like regular pattern matching: given any type One thing that differs from the GHC extension syntax is that in the example I gave the type constructor overloads are anonymous. The reason they are named in Haskell, I believe, is to allow functions to directly switch or pattern match over different named constructors of the type. I believe this is not relevant here. There's much more to this subject, I guess. It might takes some time for me to get an adequate understanding of GADTs and the implications of applying them here. |
I'll try to give examples of other applications of such types: Let's say I want to define a function that takes a value of any primitive type and returns the "zero" value corresponding to that value's type: function zeroOf(val) {
switch (typeof val) {
case "number":
return 0;
case "string":
return "";
case "boolean":
return false;
default:
throw new TypeError("The given value's type is not supported by zeroOf");
}
} How would you type this function? The best current solution offered by typescript is to use the union (Edit: yes this can be improved to use overloaded method signature, but the actual signature would still look like this, I've explained the difference in another edit below) function zeroOf(val: number | string | boolean): 0 | "" | false {
// ...
} However, the problem is that this doesn't allow "matching" a type argument to the correct member of the union. But what if it was possible to define "overloaded" type aliases? you could very naturally define: type ZeroOf<T extends number> = 0;
type ZeroOf<T extends string> = "";
type ZeroOf<T extends boolean> = false;
type ZeroOf<T> = never;
function zeroOf(readonly val: number | string | boolean): ZeroOf<typeof val> {
switch (typeof val) {
case "number": // typeof val is narrowed to number. ZeroOf<number> resolves to 0!
return 0;
case "string": // typeof val is narrowed to string. ZeroOf<string> resolves to ""!
return "";
case "boolean": // typeof val is narrowed to boolean. ZeroOf<boolean> resolves to false!
return false;
default: // typeof val is narrowed to never
// ZeroOf<never> (or any other remaining type) also resolves to never!
throw new TypeError("The given value's type is not supported by zeroOf");
}
} The combination of the overloaded type alias and literal types is so expressive here to the point where the signature almost "forces" a correct implementation of the function! Here's another example, of an evaluator function. The function takes an expression object and returns an evaluation of it. The result could be either a function eval(expression: NumberExpr | StringExpr | AdditionExpr | EqualityExpr): number | string | boolean {
if (isNumberExpr(expression) || isStringExpr(expression)) { // These could be user defined guards
return expression.terms[0];
} else if (isAdditionExpr(expression)) { // This could be a user defined guard
return eval(expression.terms[0]) + eval(expression.terms[1]);
} else if (isEqualityExpr(expression)) { // This could be a user defined guard
return eval(expression.terms[0]) === eval(expression.terms[1]);
}
} What if it was possible to represent the exact expected mapping between the given expression type and the resulting evaluated return type, in a way where the correct return type could also be enforced within the body of the function? (Edit: note this is somewhat comparable to an overloaded method signature, but more powerful: it allows the return type to be expressed clearly as a type, guarded on, checked and reused in the body of the function or outside of it. So it makes the mapping more "explicit" and encodes it as a well-defined type. Another difference is that this can also be used with anonymous functions.) type EvalResultType<T extends NumberExpr> = number;
type EvalResultType<T extends StringExpr> = string;
type EvalResultType<T extends AdditionExpr> = number;
type EvalResultType<T extends EqualityExpr> = boolean;
function eval(readonly expression: NumberExpr | StringExpr | AdditionExpr | EqualityExpr): EvalResultType<typeof expression> {
if (isNumberExpr(expression) || isStringExpr(expression)) { // These could be user defined guards
return expression.terms[0];
} else if (isAdditionExpr(expression)) { // This could be a user defined guard
return eval(expression.terms[0]) + eval(expression.terms[1]);
} else if (isEqualityExpr(expression)) { // This could be a user defined guard
return eval(expression.terms[0]) === eval(expression.terms[1]);
}
} Edit: Seems like these examples are not "convincing" enough in the context of this language, though they are the ones that are classically used with GADTs. Perhaps I've tried hard to adapt them to the limitations of Typescript's generics and they turned out too "weak". I'll try to find better ones.. |
This might go well with #12885. In particular, most of your examples would be redundant: function eval(readonly expression: NumberExpr): number;
function eval(readonly expression: StringExpr): string;
function eval(readonly expression: AdditionExpr): number;
function eval(readonly expression: EqualityExpr): boolean;
function eval(readonly expression: NumberExpr | StringExpr | AdditionExpr | EqualityExpr): string | number | boolean {
if (isNumberExpr(expression) || isStringExpr(expression)) { // These could be user defined guards
return expression.terms[0];
} else if (isAdditionExpr(expression)) { // This could be a user defined guard
return eval(expression.terms[0]) + eval(expression.terms[1]);
} else if (isEqualityExpr(expression)) { // This could be a user defined guard
return eval(expression.terms[0]) === eval(expression.terms[1]);
}
} This proposal could partially solve my function-related issue, though: interface Original {
[key: string]: (...args: any[]) => any
}
interface Wrapped {
[key: string]: (...args: any[]) => Promise<any>
}
// Partial fix - need a guard in the mapped `P` type here...
type Export<R extends Promise<any>, T extends (...args: any[]) => R> = T
type Export<R, T extends (...args: any[]) => R> = (...args: any[]) => Promise<R>
interface Mapped<T extends Original> {
[P in keyof T]: Export<T[P]>
} |
@isiahmeadows I've read your proposal but wasn't 100% sure if that what was intended. I'm aware that a non-recursive use of this feature with functions could be seen as somewhat similar to method overloading (of the form Typescript supports). The main difference is that the return values (or possibly also argument values whose type is dependent on other argument types) would have a well-defined type that is natively expressible in the language, rather than just being implicitly narrowed as a compiler "feature". Another advantage I haven't mentioned yet is that the return type could be expressed even if the argument itself is a union (or maybe a constrained generic type as well?) and could be propagated back to the caller chain: function func1(const a: string): number;
function func1(const a: number): boolean;
function func1(const a: string | number): number | boolean {
if (typeof a === "string")
return someString; // Assume the expected return type is implicitly narrowed here to number.
else if (typeof a === "number")
return someBoolean; // Assume the expected return type is implicitly narrowed here to boolean.
}
function func2(const b: string | number) { //
const x = func1(b); // How would the type of x be represented?
if (typeof b === "number") {
x; // Could x be narrowed to boolean here?
}
} In general I find the idea that a type could describe a detailed relationship between some set of inputs and outputs very powerful, and surprisingly natural. In its core, isn't that what programming is all about? If a type could, for example, capture more specific details about the mapping between say, different ranges, or sub-classes of inputs to the expected ranges/sub-classes of outputs, and those can be enforced by the compiler, it would mean mean that the compiler could effectively "prove" correctness of some aspects of the program. Perhaps encoding these relationships is not actually the most difficult aspect, but "proving" them is. I've read a bit about languages like Agda and Idris that feature dependent types but haven't really got deeply into that. It would be interesting to at least find some very limited examples of how (enforceable) dependent types would look like in Typescript. I understand that it may be significantly more challenging to implement them over impure languages like Javascript though. |
It kind of helps that in languages like Idris and Agda, pattern matching is
practically the only way to actually conditionally test things
(if-then-else are language level for them).
But yes, that declarative kind of mechanism is useful. It's why, in my
limited experience, it's easier to read complex Idris types than equivalent
Haskell or even TypeScript types. But I'm not quite as sold on how much
that actually fits in with the rest of TypeScript stylistically.
Programming language design is just as much an art form as it is a logical
point of research and practicality.
…On Tue, Dec 27, 2016, 11:42 Rotem Dan ***@***.***> wrote:
@isiahmeadows <https://github.com/isiahmeadows>
I've read your proposal but wasn't 100% sure if that what was intended.
I'm aware that a non-recursive use of this feature with functions could be
seen as somewhat similar to method overloading (of the form Typescript
supports). The main difference is that the return values (or possibly also
argument values whose type is dependent on other argument types) would have
a well-defined type that is natively expressible in the language, rather
than just being implicitly narrowed as a compiler "feature".
Another advantage I haven't mentioned yet is that the return type could be
expressed even if the argument itself is a union (or maybe a constrained
generic type as well?) and could be propagated back to the caller chain:
function func1(const a: string): number;function func1(const a: number): boolean;function func1(const a: string | number): number | boolean {
if (typeof a === "string")
return someString; // Assume the expected return type is implicitly narrowed here to number.
else if (typeof a === "number")
return someBoolean; // Assume the expected return type is implicitly narrowed here to boolean.
}
function func2(const b: string | number) { //
const x = func1(b); // How would the type of x be represented?
if (typeof b === "number") {
x; // Could x be narrowed to boolean here?
}
}
In general I find the idea that a type could describe a detailed
relationship between some set of inputs and outputs very powerful, and
surprisingly natural. In its core, isn't that what programming is all
about? If a type could, for example, capture more specific details about
the mapping between say, different ranges, or sub-classes of inputs to the
expected ranges/sub-classes of outputs, and those can be enforced by the
compiler, it would mean mean that the compiler could effectively "prove"
correctness of some aspects of the program.
Perhaps encoding these relationships is not actually the most difficult
aspect, but "proving" them is. I've read a bit about languages like Agda
and Idris that feature dependent types but haven't really got deeply into
that. It would be interesting to at least find some very limited examples
of how (enforceable) dependent types would look like in Typescript. I
understand that it may be significantly more challenging to implement them
over impure languages like Javascript though.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#12424 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AERrBHAzma1qmDqp36OT6UeRTNpgHRmUks5rMT_xgaJpZM4K4ssm>
.
|
@isiahmeadows Edit: Re-reading the responses, I think I might have been misunderstood: it was definitely not my intention to require the programmer to explicitly declare the complex return type - that would be tedious, but that the compiler could infer an "explicit" (in the sense of being well defined in the type system) type for the return value rather than just implicitly narrowing it as a localized "feature". I've also tried to come up with a more concise "abbreviated" form for the guarded type. I've tried to re-read #12885 but I'm still not 100% sure if it describes the same issue as I mentioned here. It seems like it tries to address an aspect of overload inference that is somewhat related, but more like the "flip-side" of this issue: // Unfortunately the parameter has to be 'const' or 'readonly' here for the
// issue to be easily addressable. I don't believe these modifiers are currently
// supported for function parameters but I'm using 'const' for illustration:
function func(const a: string): number;
function func(const a: number): boolean;
function func(const a: string | number): number | boolean {
if (typeof a === "string") {
return true; // <-- This should definitely be an error, but currently isn't.
}
} The weak return type checking in the body of overloaded function is a real world problem I've encountered many times and seems very worthy of attention. It might be possible to fix this through an implicit compiler inference "feature", but I felt that guarded polymorphic types could take it even a step further: function func(const a: string): number;
function func(const a: number): boolean;
function func(const a: boolean): number[];
function func(const a: string | number | boolean) { // The return type is omitted by
// the programmer. Instead, it it automatically
// generated by the compiler.
if (typeof a === "string") {
return true; // <-- Error here
}
} The generated signature could look something like: // (The generated return type is concisely expressed using a
// suggested abbreviated form for a guarded type)
function func(const a: string | number | boolean):
<typeof a extends string>: number, <typeof a extends number>: boolean, <typeof a extends boolean>: number[]; The abbreviated form (which is currently still in development), when written as a type alias, would look like: type FuncReturnType = <T extends string>: number, <T extends number>: boolean, <T extends boolean>: number[]; As I've mentioned the type can be propagated back to the callers in an unresolved form if the argument type itself is a union, and it can even be partially resolved if that union is a strict sub-type of the parameter type: // The argument type 'b' is a union, but narrower:
function callingFunc(const b: "hello" | boolean) {
return func(b);
} The signature of the calling function is generated based on a reduction of the existing guarded type to the more constrained union and substitution of the identifier used in the signature ( function callingFunc(const b: "hello" | boolean):
<typeof b extends "hello">: number, <typeof b extends boolean>: number[]; Perhaps this may seem, at first, like an "overly-engineered" solution, that takes it quite far but doesn't actually produce adequate amount of value in practice. It may be the case (though I'm not at all totally sure) if only simple types like function operation1(const x: number<0..Infinity>): number<0..1>;
function operation1(const x: number<-Infinity..0>): number<-1..0>;
function operation1(const x: number) {
// ...
} Now what if multiple functions like these are composed? function operation2(const x: number<0..10>): number<-10..0>;
function operation2(const x: number<-10..0>): number<0..10>;
function operation2(const x: number<-10..10>) {
// ...
}
function operation3(const x: number<-10..10>): {
return operation1(operation2(x));
} To generate a signature for function operation3(const x: number<-10..10>):
<typeof x extends number<-10..0>>: number<0..1>, <typeof x extends number<0..10>>: number<-1..0> I guess it wouldn't look as beautiful in Typescript as it would look with a more concise syntax like Haskell's, and the lack of pattern-matching, assurance of immutability of variables and purity of functions may reduce the usability of the feature, but I feel there's still a lot of potential here to be explored, especially since Typescript already performs disambiguation of unions using run-time guards, and has a variant of function overloading that is very atypical when compared with common statically typed languages. Edits: I've corrected some errors in the text, so re-read if you only read the e-mail's version |
To clarify #12885, it focuses on expanding the type inference for callers only, and it is very highly specific to overloads. I intentionally laid that focus, because I wanted to limit its scope. (It's much easier and more likely that a proposal will get somewhere when you keep it down to a single unit.) So it is somewhat like a flip side, but the inverse of my proposal, using those same links to deduce the correct return type from the differing parameter type, would in fact be what you're looking for here. It's an abstract enough concept it's hard to put it into precise terminology without delving into incomprehensible, highly mathematical jargon you'd be lucky to even hear Haskellers using. |
It would be nice for this conditions to also allow any function matching. Practical example with attempt to properly type a = function() {}
a.prototype.b = 3;
a.prototype.c = function() {};
stub = sinon.createStubInstance(a);
console.log(typeof stub.c.getCall); // 'function', c is of type SinonStub
console.log(typeof stub.b); // 'number' - b is still number, not SinonStub To type it correctly we need the ability to match any function Original discussion in DefinitelyTyped repo: DefinitelyTyped/DefinitelyTyped#13522 (comment) |
One other area where conditionals would help: The native So, in order to properly type that, you have to constrain it to not include thenables. |
I noticed that: function func(const a: string | number | boolean):
<typeof a extends string>: number, <typeof a extends number>: boolean, <typeof a extends boolean>: number[]; Can be simplified and shortened even further using the already existing value type assertion expression syntax function func(const a: string | number | boolean):
<a is string>: number, <a is number>: boolean, <a is boolean>: number[]; The general idea is that I hope that having a more accessible and readable syntax would improve the chance of this being seriously considered for adoption. Another thing to note is that the guarded type (* I mean, at least in the example I gave - this may not be true in general, but it seems like when used with overloaded function parameters that should mostly be the case, though more investigation is needed here) |
@rotemdan I like the general idea of that better, for explicitly typing my idea in #12885. I have my reservations about the syntax, though. Maybe something like this, a little more constraint-oriented with better emphasis on the union? It would also allow more complex relations syntactically down the road. // `a`'s type is actually defined on the right, not the left
function func(a: *): (
a is string = number |
a is number = boolean |
a is boolean = number[]
);
// Equivalent overload
function func(a: string): number
function func(a: number): boolean
function func(a: boolean): number[]
// Nearest supertype of the return type within the current system:
number | boolean | number[] You could expand on this further down the road, inferring variable types to effectively reify overloads in the type system. In fact, this could be made also a lambda return type, unifying lambdas and function overloads. // 2-ary overload with different return types
function func(a: *, b: *): (
a is string & b is string = number |
a is number & b is number = boolean |
a is boolean & b is string = number[]
)
// Actual type of `func`
type Func = (a: *, b: *) => (
a is string & b is string = number |
a is number & b is number = boolean |
a is boolean & b is string = number[]
)
// Equivalent overload
function func(a: string, b: string): number
function func(a: number, b: number): boolean
function func(a: boolean, b: string): number[] I could also see this expanded to the type level and unified there as well, although I'd prefer to write that out in a more detailed proposal. |
@isiahmeadows This was just an initial attempt at coming up with a secondary shorter syntax semantically equivalent to the "overload-like" syntax: type MyType<T extends string> = number;
type MyType<T extends number> = boolean;
type MyType<T extends boolean> = number[]; But where instead of using a generic parameter, the guard is bound to the type of a particular variable. In the longer version it would look like this const a: string | number | boolean = ...;
type MyType<a is string> = number;
type MyType<a is number> = boolean;
type MyType<a is boolean> = number[]; I wanted the shorter syntax to be easily written (or inferred) in return types or normal positions. I used a comma ( This is how it looks with commas: const a: string | number | boolean = ...
let b: <a is string>: number, <a is number>: boolean, <a is boolean>: number[]; And with the vertical bar (union-like) syntax it would look like: let b: <a is string>: number | <a is number>: boolean | <a is boolean>: number[]; And with multiple parameters: let b: <a is string, b is boolean>: number |
<a is string, b is number>: boolean |
<a is boolean>: number[]; I don't think this looks bad at all. If you think that the fact it is order-sensitive isn't going create confusion with regular unions than it seams reasonable to me as well. I used the angled brackets because I wanted to preserve the analogy from the "overload-like" syntax and maintain the intuitive sense that these are arguments for a type rather than a function of some sort. I used the colons ( So in a function return type, the union-like syntax would look like: function func(const a: string | number | boolean):
<a is string>: number | <a is number>: boolean | <a is boolean>: number[]; I'm not particularly "attached" to this syntax though. I think what you proposed was reasonable as well. (this is a bit off-topic but I felt I had to say it:) |
I'm currently working out a shotgun that'll also kill a few others, including subtraction types. |
@isiahmeadows I didn't think the ampersand ( Maybe I'll try to illustrate better where I was going to with the function syntax. Here's another variant of my notation, closer to yours, as I used So this is what the programmer annotates (this is how the actual code looks like): function func(a: string, b: string): number;
function func(a: string, b: number): boolean;
function func(a: boolean, b: number): number[];
function func(a, b) {
} And this is how it is inferred by the compiler within the body of the function: function func(a: string, b: string): number;
function func(a: string, b: number): boolean;
function func(a: boolean, b: number): number[];
function func(a: string | boolean, b: (<a is string> = string | number) | number):
<a is string, b is string> = number |
<a is string, b is number> = boolean |
<a is boolean> = number[] // Note `b` is not needed here to disambiguate the return type You might have noticed I used this strange annotation: (<a is string> = string | number) | number It is an experimental combination of a "guarded" union member and a non-guarded union member. It could have also been written as something like: (<a is string> = string | number) | (<*> = number) Where Another advantage of the type alias like notation is that it makes it possible to combine both assertions on types ( const a: number | string | boolean;
type GuardedType<T extends string[], a is number> = ...
type GuardedType<T extends number, a is boolean> = ... (I guess at this point it's too early to determine how useful this would be in practice, this is all very preliminary) |
You can currently do some |
Does it map over unions? That's one of the stumbling blocks that I didn't think we could overcome without some changes to the compiler: declare var test: RecursiveReadonly<{ foo: number | number[] }>
if (typeof test.foo != 'number') {
test.foo[0] = 1; // no error?
} |
@jcalz ah, good catch! Yeah, can't think of a way to support unions this way :/ |
Yeah, so We do not yet have anything like union iteration to address that today. We'd want an I'd really need to document which types are union-proof in which parameters, as this won't be the only type that'd break on this. I'm actually thinking in this case the globals augmentation method to identify prototypes might do better in terms of staying union-proof than my |
I'm trying to implement a type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
interface I {
fn: (a: string) => void
n: number
}
let v: DeepPartial<I> = {
fn: (a: number) => {}, // The compiler is happy -- bad.
n: '' // The compiler complains -- good.
} Is this something that the current proposal could solve as well? |
@inad9300 I too stumbled upon this thread with the intent of making a DeepPartial type. |
@inad9300 @vultix type False = '0';
type True = '1';
type If<C extends True | False, Then, Else> = { '0': Else, '1': Then }[C];
type Diff<T extends string, U extends string> = (
{ [P in T]: P } & { [P in U]: never } & { [x: string]: never }
)[T];
type X<T> = Diff<keyof T, keyof Object>
type Is<T, U> = (Record<X<T & U>, False> & Record<any, True>)[Diff<X<T>, X<U>>]
type DeepPartial<T> = {
[P in keyof T]?: If<Is<Function & T[P], Function>, T[P], DeepPartial<T[P]>>
} I haven't tested it thoroughly but it worked with the example you provided. Edit: type I = DeepPartial<{
fn: () => void,
works: {
foo: () => any,
},
fails: {
apply: any,
}
}>
// equivalent to:
type J = {
fn?: () => void,
works?: {
foo?: () => any,
},
fails?: {
apply: any // not optional
}
} |
@tao-cumplido What you did there is mind tangling, but admirable. It's a real pity that is not covering all the cases, but it works better than the common approach. Thank you! |
@inad9300 type DeepPartial<T> = {
[P in keyof T]?: If<Is<T[P], object>, T[P], DeepPartial<T[P]>>
} It no longer tests against |
@jcalz @inad9300 @vultix @tao-cumplido: I added a |
#12114 added mapped types, including recursive mapped types. But as pointed out by @ahejlsberg
I couldn't find an existing issue with a feature request.
Conditional mapping would greatly improve the ergonomics of libraries like Immutable.js.
The text was updated successfully, but these errors were encountered: