Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constraints are not considered for Conditional Types #31096

Closed
hediet opened this issue Apr 24, 2019 · 5 comments
Closed

Constraints are not considered for Conditional Types #31096

hediet opened this issue Apr 24, 2019 · 5 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@hediet
Copy link
Member

hediet commented Apr 24, 2019

TypeScript Version: 3.5.0-dev.20190424

Code

function f<
	TType extends { p: string },
>(
	bla: TType
): void {
	const x: TType['p'] = 'test'; // typechecks
	const y: TType["p"] extends string ? string : number = "test"; // does not
}

Expected behavior:
It typechecks.

Actual behavior:
It does not:

Type '"test"' is not assignable to type 'TType["p"] extends string ? string : number'.
const y: TType["p"] extends string ? string : number

The assignment to x however typechecks.

Playground Link

@jack-williams
Copy link
Collaborator

This is working as intended. Conditional type resolution ignores type parameter constraints to prevent undesirable resolutions that violate transitivity. See this comment:

// Return trueType for a definitely true extends check. We check instantiations of the two
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// doesn't immediately resolve to 'string' instead of being deferred.

A concrete example:

function f<
	TType extends { p: any },
>(
	bla: TType
): TType["p"] extends string ? string : number {	
	return "test"; // If we used the constraint this would be legal.
}

const res: number = f({ p: 3 }); // res === "test";

There are cases where using the constraint is sound but it involves exploring the constraint type.

@hediet
Copy link
Member Author

hediet commented Apr 25, 2019

I've managed to overcome this issue by replacing conditional types with map-types:

function f<
	TType extends { kind: "a" },
>(
	bla: TType
): void {
	const x: TType['kind'] = 'a'; // typechecks
	let y: { a: string, b: number }[TType["kind"]]; // does not
	y = "test";
	y = 4;
}

@jack-williams So this restriction is only because of any?

@jack-williams
Copy link
Collaborator

jack-williams commented Apr 25, 2019

So this restriction is only because of any?

In general the root of the issue is that assignability is not a transitive relation, however any is the main culprit. There are four violations of transitivity that I know of; there may be more:

(Thanks to @rkirov for number four).

I use <: for assignability

  1. string <: any and any <: number, but it's not the case that string <: number.
  2. number <: Object and Object <: object, but it's not the case that number <: object.
  3. { x: number; y: number } <: { x: number } and { x: number } <: { x: number; y?: string } but it's not the case that { x: number; y: number } <: { x: number; y?: string }.
  4. Callbacks with void:
    • () => unknown <: () => void and () => void <: () => (boolean | void),
    • but it's not the case that () => unknown <: () => (boolean | void).

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Apr 25, 2019
@hediet hediet closed this as completed May 1, 2019
@rkirov
Copy link
Contributor

rkirov commented Sep 19, 2019

@jack-williams I ended up here looking for 'transitivity failures', so I got one more for you (no 'any's)

declare let c: {a(): {}|null|void};
declare let b: {a(): void};
declare let a: {a(): unknown};

b = a;
c = b;
// but 
c = a;  // error

Also that means it is not worth filing a bug, given the amount of other known failures.

@shicks
Copy link
Contributor

shicks commented Nov 26, 2019

Since this seems to be as good a place as any to document transitivity failures and it's not listed yet, there's also some good ones with optional parameters. This one is fun because you can run it both directions.

declare let x: (a?: number) => void;
declare let y: () => void;
declare let z: (a?: string) => void;

y = z;
x = y;
x = z; // error

y = x;
z = y;
z = x; // error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

5 participants