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

Closed
barake opened this Issue Nov 21, 2016 · 62 comments

Comments

Projects
None yet
@barake

barake commented Nov 21, 2016

#12114 added mapped types, including recursive mapped types. But as pointed out by @ahejlsberg

Note, however, that such types aren't particularly useful without some form of conditional type that makes it possible to limit the recursion to selected kinds of types.

type primitive = string | number | boolean | undefined | null;
type DeepReadonly<T> = T extends primitive ? T : DeepReadonlyObject<T>;
type DeepReadonlyObject<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>;
};

I couldn't find an existing issue with a feature request.

Conditional mapping would greatly improve the ergonomics of libraries like Immutable.js.

@Artazor

This comment has been minimized.

Show comment
Hide comment
@Artazor

Artazor Nov 22, 2016

Contributor

Surely it should be implemented. However, need to find an appropriate syntax. There is possible clash with other proposal: #4890

Contributor

Artazor commented Nov 22, 2016

Surely it should be implemented. However, need to find an appropriate syntax. There is possible clash with other proposal: #4890

@gentoo90

This comment has been minimized.

Show comment
Hide comment
@gentoo90

gentoo90 Nov 22, 2016

Another possible use case is typeguarding Knockout.js mappings, which needs choosing between KnockoutObservable and KnockoutObservableArray.

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>

KoItem should be expanded to

type KoItem = {
    id: KnockoutObservable<number>;
    name: KnockoutObservable<string>;
    subitems: KnockoutObservableArray<string>;
}

gentoo90 commented Nov 22, 2016

Another possible use case is typeguarding Knockout.js mappings, which needs choosing between KnockoutObservable and KnockoutObservableArray.

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>

KoItem should be expanded to

type KoItem = {
    id: KnockoutObservable<number>;
    name: KnockoutObservable<string>;
    subitems: KnockoutObservableArray<string>;
}
@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Dec 21, 2016

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 T the first pattern (T extends Primitive) is tested. If the type matches the constraint, then it is resolved, if not, it continues to the next pattern (<T>). Since there is no constraint on the second one, it acts similarly to otherwise or default and would accept anything that doesn't match the previous ones (Note the order of statements matters here: similarly to class method overloading it probably must be enforced that overloaded type declarations of similar identifiers strictly follow each other, to prevent accidental redefinition of types)

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.

rotemdan commented Dec 21, 2016

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 T the first pattern (T extends Primitive) is tested. If the type matches the constraint, then it is resolved, if not, it continues to the next pattern (<T>). Since there is no constraint on the second one, it acts similarly to otherwise or default and would accept anything that doesn't match the previous ones (Note the order of statements matters here: similarly to class method overloading it probably must be enforced that overloaded type declarations of similar identifiers strictly follow each other, to prevent accidental redefinition of types)

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.

@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Dec 22, 2016

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 number | string | boolean as paramter type and and the union 0 | "" | false as return type:

(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 number, a string, or a boolean. This would be the "normal" way to describe this:

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..

rotemdan commented Dec 22, 2016

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 number | string | boolean as paramter type and and the union 0 | "" | false as return type:

(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 number, a string, or a boolean. This would be the "normal" way to describe this:

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..

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Dec 27, 2016

Contributor

@rotemdan

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]>
}
Contributor

isiahmeadows commented Dec 27, 2016

@rotemdan

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]>
}
@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Dec 27, 2016

@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.

rotemdan commented Dec 27, 2016

@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.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Dec 27, 2016

Contributor
Contributor

isiahmeadows commented Dec 27, 2016

@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Dec 28, 2016

@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 (a) with the target caller's parameter (b).

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 string and number are involved, but what if the overloads described more fine-grained aspects of the parameters? like refinement types:

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 operation3 the compiler could "condense" this complex propagation of constrained unknowns into a simpler resulting signature:

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

rotemdan commented Dec 28, 2016

@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 (a) with the target caller's parameter (b).

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 string and number are involved, but what if the overloads described more fine-grained aspects of the parameters? like refinement types:

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 operation3 the compiler could "condense" this complex propagation of constrained unknowns into a simpler resulting signature:

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

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Dec 28, 2016

Contributor

@rotemdan

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.

Contributor

isiahmeadows commented Dec 28, 2016

@rotemdan

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.

@zuzusik

This comment has been minimized.

Show comment
Hide comment
@zuzusik

zuzusik Dec 30, 2016

It would be nice for this conditions to also allow any function matching.

Practical example with attempt to properly type sinon.createStubInstance:

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
Seems like Function type should do this trick, right?
Just want to make sure it will work correctly with this feature

Original discussion in DefinitelyTyped repo: DefinitelyTyped/DefinitelyTyped#13522 (comment)

zuzusik commented Dec 30, 2016

It would be nice for this conditions to also allow any function matching.

Practical example with attempt to properly type sinon.createStubInstance:

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
Seems like Function type should do this trick, right?
Just want to make sure it will work correctly with this feature

Original discussion in DefinitelyTyped repo: DefinitelyTyped/DefinitelyTyped#13522 (comment)

@zuzusik zuzusik referenced this issue Dec 30, 2016

Merged

[sinon] properly type sinon.createStubInstance #13522

8 of 8 tasks complete
@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Dec 31, 2016

Contributor

One other area where conditionals would help: The native Promise type should never accept a thenable in its generic parameter, since JavaScript does maintain the invariant that the argument to then callbacks are always coerced down to a single lifted value.

So, in order to properly type that, you have to constrain it to not include thenables.

Contributor

isiahmeadows commented Dec 31, 2016

One other area where conditionals would help: The native Promise type should never accept a thenable in its generic parameter, since JavaScript does maintain the invariant that the argument to then callbacks are always coerced down to a single lifted value.

So, in order to properly type that, you have to constrain it to not include thenables.

@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Jan 1, 2017

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 val is T:

function func(const a: string | number | boolean): 
         <a is string>: number, <a is number>: boolean, <a is boolean>: number[];

The general idea is that a is string represents a boolean-like assertion "type" just like T extends string (only it is bound to a specific variable), so it seems reasonable to allow both at that position.

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 <a is string>: number, <a is number>: boolean, <a is boolean>: number[] can be seen as a subtype of the more general type number | boolean | number[]* so whenever it isn't possible to resolve it (say when the bound variable went out of scope and wasn't substituted by anything), it can always be cast back to its corresponding union supertype.

(* 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 commented Jan 1, 2017

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 val is T:

function func(const a: string | number | boolean): 
         <a is string>: number, <a is number>: boolean, <a is boolean>: number[];

The general idea is that a is string represents a boolean-like assertion "type" just like T extends string (only it is bound to a specific variable), so it seems reasonable to allow both at that position.

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 <a is string>: number, <a is number>: boolean, <a is boolean>: number[] can be seen as a subtype of the more general type number | boolean | number[]* so whenever it isn't possible to resolve it (say when the bound variable went out of scope and wasn't substituted by anything), it can always be cast back to its corresponding union supertype.

(* 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)

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jan 2, 2017

Contributor

@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.

Contributor

isiahmeadows commented Jan 2, 2017

@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.

@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Jan 2, 2017

@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 (,) as a connector, although I also considered a vertical bar (|). The reason I chose the comma was that I wanted to make sure it is seen as order-sensitive. The union syntax is not order-sensitive in Typescript so I wanted to avoid that confusion:

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 (:) instead of equals (=) to make sure it isn't read such that there's an assignment from a type into the assertion type val is T. It looks a bit out-of-place to me to use an assignment-like operator in a type expression.

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 wish though, that the talented people at the Typescript team would actively involve themselves and contribute to discussions, rather than just acting mostly as passive bystanders. I think they maybe don't realize that if they did share their own ideas with the community, the community might be able to improve on them and some sort of "symbiosis" could form. Right now they are operating like they have their own closed "guild", and in some way the community is required to match their standards without really having them giving any incremental feedback. Until they make up their mind in their closed meetings, and then it is too late to change. There's something a bit patronizing about this. I just hope we're not wasting our time here.

rotemdan commented Jan 2, 2017

@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 (,) as a connector, although I also considered a vertical bar (|). The reason I chose the comma was that I wanted to make sure it is seen as order-sensitive. The union syntax is not order-sensitive in Typescript so I wanted to avoid that confusion:

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 (:) instead of equals (=) to make sure it isn't read such that there's an assignment from a type into the assertion type val is T. It looks a bit out-of-place to me to use an assignment-like operator in a type expression.

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 wish though, that the talented people at the Typescript team would actively involve themselves and contribute to discussions, rather than just acting mostly as passive bystanders. I think they maybe don't realize that if they did share their own ideas with the community, the community might be able to improve on them and some sort of "symbiosis" could form. Right now they are operating like they have their own closed "guild", and in some way the community is required to match their standards without really having them giving any incremental feedback. Until they make up their mind in their closed meetings, and then it is too late to change. There's something a bit patronizing about this. I just hope we're not wasting our time here.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jan 2, 2017

Contributor

I'm currently working out a shotgun that'll also kill a few others, including subtraction types.

Contributor

isiahmeadows commented Jan 2, 2017

I'm currently working out a shotgun that'll also kill a few others, including subtraction types.

@rotemdan

This comment has been minimized.

Show comment
Hide comment
@rotemdan

rotemdan Jan 2, 2017

@isiahmeadows

I didn't think the ampersand (&) was a good choice for a connector two expressions that are closer to booleans. Maybe the double ampersand (&&) would have been better there, I guess. I wasn't exactly sure what the * meant as well (existential type?). I thought it looked interesting though, but maybe too "abstract" or "enigmatic" to the average programmer. I understand you tried to remove redundancies and unify the parameter types and the return type somehow, but there are several reasons why that wouldn't always be needed or the best thing to do.

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 = instead : (I'm starting to think it doesn't look as bad as I initially thought). Having the angled brackets would maybe allow to parse it more easily. It also makes it look like "anonymous" type aliases. I'm using the regular function overloading syntax, and I wanted to now show how it would look once these guarded types are inferred by the compiler.

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 <*> denotes "the rest" or "any other case".

Another advantage of the type alias like notation is that it makes it possible to combine both assertions on types (T extends U) and assertions on values (val is T):

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)

rotemdan commented Jan 2, 2017

@isiahmeadows

I didn't think the ampersand (&) was a good choice for a connector two expressions that are closer to booleans. Maybe the double ampersand (&&) would have been better there, I guess. I wasn't exactly sure what the * meant as well (existential type?). I thought it looked interesting though, but maybe too "abstract" or "enigmatic" to the average programmer. I understand you tried to remove redundancies and unify the parameter types and the return type somehow, but there are several reasons why that wouldn't always be needed or the best thing to do.

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 = instead : (I'm starting to think it doesn't look as bad as I initially thought). Having the angled brackets would maybe allow to parse it more easily. It also makes it look like "anonymous" type aliases. I'm using the regular function overloading syntax, and I wanted to now show how it would look once these guarded types are inferred by the compiler.

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 <*> denotes "the rest" or "any other case".

Another advantage of the type alias like notation is that it makes it possible to combine both assertions on types (T extends U) and assertions on values (val is T):

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)

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Jan 2, 2017

Contributor

@rotemdan I've come up with a concrete, slightly smaller-scoped and differently-scoped proposal in #13257. Basically, I'm granting the ability to statically assert many more things by introducing constraint types, to kill this and several others simultaneously.

Contributor

isiahmeadows commented Jan 2, 2017

@rotemdan I've come up with a concrete, slightly smaller-scoped and differently-scoped proposal in #13257. Basically, I'm granting the ability to statically assert many more things by introducing constraint types, to kill this and several others simultaneously.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 May 29, 2017

Contributor

This issue has two sides:

  • proper (pattern matching) conditionals (simple ones usable today, see #14833 / my HasKey)
  • type-level type checks

#6606 elegantly fixes both without new syntax:

interface isT<T> {
  (v: T): 1;
  (v: any): 0;
}
// type Matches<V, T> = typeof isT<T>(V);
// ^ fails until #6606
Contributor

tycho01 commented May 29, 2017

This issue has two sides:

  • proper (pattern matching) conditionals (simple ones usable today, see #14833 / my HasKey)
  • type-level type checks

#6606 elegantly fixes both without new syntax:

interface isT<T> {
  (v: T): 1;
  (v: any): 0;
}
// type Matches<V, T> = typeof isT<T>(V);
// ^ fails until #6606
@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Jun 21, 2017

Contributor

@remojansen: I'm actually a bit surprised restricting the T type and overloading get doesn't cut it:

interface Immutable<T extends { [k: string]: any } | any[]> {
  get: {
    <K extends keyof T>(key: K): Immutable<T[K]>;
    <K extends keyof T>(key: K): T[K];
  }
  set: <K extends keyof T>(key: K, val: T[K]) => Immutable<T>;
  toJS(): T;
}

Just that function application (#6606) would tackle instanceof and more though (including using overloads as pattern-match style conditionals on steroids).

That said, I'd intuitively assumed instanceof and some others (spreads ..., operators for primitive literals, assertion operator !) to be available on the type level as well.
But I'm now of the opinion that just two additions (function application + spreads) could combine to enable doing just about anything (ref).

Contributor

tycho01 commented Jun 21, 2017

@remojansen: I'm actually a bit surprised restricting the T type and overloading get doesn't cut it:

interface Immutable<T extends { [k: string]: any } | any[]> {
  get: {
    <K extends keyof T>(key: K): Immutable<T[K]>;
    <K extends keyof T>(key: K): T[K];
  }
  set: <K extends keyof T>(key: K, val: T[K]) => Immutable<T>;
  toJS(): T;
}

Just that function application (#6606) would tackle instanceof and more though (including using overloads as pattern-match style conditionals on steroids).

That said, I'd intuitively assumed instanceof and some others (spreads ..., operators for primitive literals, assertion operator !) to be available on the type level as well.
But I'm now of the opinion that just two additions (function application + spreads) could combine to enable doing just about anything (ref).

@AlexGalays

This comment has been minimized.

Show comment
Hide comment
@AlexGalays

AlexGalays Jul 23, 2017

This is useful. As it stands, the recursive mapped type will dumbly apply the mapping to each level and property, be it an object, an Array or a primitive.

Funnily, I need exactly the DeepReadOnly from the first example but Arrays would require special treatment for an entire JSON tree to be mapped to read only: As it stands, it just makes the Array keys readonly (map, reduce, etc) and it even loses the index type.

AlexGalays commented Jul 23, 2017

This is useful. As it stands, the recursive mapped type will dumbly apply the mapping to each level and property, be it an object, an Array or a primitive.

Funnily, I need exactly the DeepReadOnly from the first example but Arrays would require special treatment for an entire JSON tree to be mapped to read only: As it stands, it just makes the Array keys readonly (map, reduce, etc) and it even loses the index type.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Sep 8, 2017

Contributor

@isiahmeadows: I agree; I upvoted @jcalz's #16386 now.

Contributor

tycho01 commented Sep 8, 2017

@isiahmeadows: I agree; I upvoted @jcalz's #16386 now.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Sep 9, 2017

Contributor

@jcalz: I think I got past your evaluation issue (#12424 (comment)); the following now works for me at #17961:

interface isT<T> {
  (v: T): '1';
  (v: any): '0';
}
type Matches<V, T> = isT<T>(V);
type isBool = isT<boolean>;
let falseBool: isBool(false); // 1
let strBool: isBool(string); // 0

Doesn't address mapping unions, but it may help make the global pollution unnecessary.

Contributor

tycho01 commented Sep 9, 2017

@jcalz: I think I got past your evaluation issue (#12424 (comment)); the following now works for me at #17961:

interface isT<T> {
  (v: T): '1';
  (v: any): '0';
}
type Matches<V, T> = isT<T>(V);
type isBool = isT<boolean>;
let falseBool: isBool(false); // 1
let strBool: isBool(string); // 0

Doesn't address mapping unions, but it may help make the global pollution unnecessary.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Sep 11, 2017

Contributor

I now made type calls union-proof (can always map over union inputs) to solve #17077. Still having trouble getting the full thing here to work though.

@streetrider:

TS should support «literal» Symbol types and presenting symbols in interfaces. I believe this would be enough.

Actually you can, like { a: 1, [Symbol.unscopables]: 2 }.

Contributor

tycho01 commented Sep 11, 2017

I now made type calls union-proof (can always map over union inputs) to solve #17077. Still having trouble getting the full thing here to work though.

@streetrider:

TS should support «literal» Symbol types and presenting symbols in interfaces. I believe this would be enough.

Actually you can, like { a: 1, [Symbol.unscopables]: 2 }.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Sep 13, 2017

Contributor

There's another attempt at a recursive readonly at #10725.

Contributor

tycho01 commented Sep 13, 2017

There's another attempt at a recursive readonly at #10725.

@jcalz

This comment has been minimized.

Show comment
Hide comment
@jcalz

jcalz Sep 13, 2017

Contributor

Wow, that's pretty much the same; isn't it: global augmentation.

Contributor

jcalz commented Sep 13, 2017

Wow, that's pretty much the same; isn't it: global augmentation.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Sep 14, 2017

Contributor

@jcalz: this thread had been linked there before, so that'd make sense yeah. :)

Contributor

tycho01 commented Sep 14, 2017

@jcalz: this thread had been linked there before, so that'd make sense yeah. :)

@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani Nov 29, 2017

You can currently do some ifs to assert certain types (well, not types exactly but type shapes). See my example of a recursive readonly that follows inside Arrays, but doesn't affect booleans/strings/numbers: Playground.
Thanks to @tycho01 for some of the helpers.

niieani commented Nov 29, 2017

You can currently do some ifs to assert certain types (well, not types exactly but type shapes). See my example of a recursive readonly that follows inside Arrays, but doesn't affect booleans/strings/numbers: Playground.
Thanks to @tycho01 for some of the helpers.

@jcalz

This comment has been minimized.

Show comment
Hide comment
@jcalz

jcalz Nov 29, 2017

Contributor

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?
}
Contributor

jcalz commented Nov 29, 2017

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?
}
@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani Nov 29, 2017

@jcalz ah, good catch! Yeah, can't think of a way to support unions this way :/

niieani commented Nov 29, 2017

@jcalz ah, good catch! Yeah, can't think of a way to support unions this way :/

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Nov 29, 2017

Contributor

Yeah, so IsArrayType is not union-proof in T. It relies on this DefinitelyYes, which here intends to aggregate the check results of different ArrayPrototypeProperties keys, but logically the results should remain separated for different T union elements.

We do not yet have anything like union iteration to address that today. We'd want an IsUnion too in that case, but the best I came up with could only distinguish string literals vs. unions thereof.

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 IsArrayType.

Contributor

tycho01 commented Nov 29, 2017

Yeah, so IsArrayType is not union-proof in T. It relies on this DefinitelyYes, which here intends to aggregate the check results of different ArrayPrototypeProperties keys, but logically the results should remain separated for different T union elements.

We do not yet have anything like union iteration to address that today. We'd want an IsUnion too in that case, but the best I came up with could only distinguish string literals vs. unions thereof.

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 IsArrayType.

@inad9300

This comment has been minimized.

Show comment
Hide comment
@inad9300

inad9300 Dec 17, 2017

I'm trying to implement a DeepPartial<T> interface which recursively makes optional all the properties of the given type. I've noticed that function signatures are not checked by TypeScript after applying it, i.e.

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 commented Dec 17, 2017

I'm trying to implement a DeepPartial<T> interface which recursively makes optional all the properties of the given type. I've noticed that function signatures are not checked by TypeScript after applying it, i.e.

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?

@vultix

This comment has been minimized.

Show comment
Hide comment
@vultix

vultix Jan 5, 2018

@inad9300 I too stumbled upon this thread with the intent of making a DeepPartial type.

vultix commented Jan 5, 2018

@inad9300 I too stumbled upon this thread with the intent of making a DeepPartial type.

@tao-cumplido

This comment has been minimized.

Show comment
Hide comment
@tao-cumplido

tao-cumplido Jan 10, 2018

@inad9300 @vultix
I was working on something with the concepts discussed here and realized it might work to make a DeepPartial that works with functions too.

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:
I just realized that it doesn't work in every case. Specifically if the nested object's keyset is a subset of Function's keyset.

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 commented Jan 10, 2018

@inad9300 @vultix
I was working on something with the concepts discussed here and realized it might work to make a DeepPartial that works with functions too.

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:
I just realized that it doesn't work in every case. Specifically if the nested object's keyset is a subset of Function's keyset.

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
    }
}
@inad9300

This comment has been minimized.

Show comment
Hide comment
@inad9300

inad9300 Jan 20, 2018

@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 commented Jan 20, 2018

@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!

@tao-cumplido

This comment has been minimized.

Show comment
Hide comment
@tao-cumplido

tao-cumplido Jan 21, 2018

@inad9300
I found a version that works better:

type DeepPartial<T> = {
    [P in keyof T]?: If<Is<T[P], object>, T[P], DeepPartial<T[P]>>
}

It no longer tests against Function which solves the problem above. I still found another case that doesn't work (it didn't in the first version either): when you create a union with a string-indexed type it fails. But the upcoming conditional types should allow a straightforward DeepPartial.

tao-cumplido commented Jan 21, 2018

@inad9300
I found a version that works better:

type DeepPartial<T> = {
    [P in keyof T]?: If<Is<T[P], object>, T[P], DeepPartial<T[P]>>
}

It no longer tests against Function which solves the problem above. I still found another case that doesn't work (it didn't in the first version either): when you create a union with a string-indexed type it fails. But the upcoming conditional types should allow a straightforward DeepPartial.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Feb 11, 2018

Contributor

@jcalz @inad9300 @vultix @tao-cumplido: I added a DeepPartial based on the new conditional types in #21316.

Contributor

tycho01 commented Feb 11, 2018

@jcalz @inad9300 @vultix @tao-cumplido: I added a DeepPartial based on the new conditional types in #21316.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.