microsoft / TypeScript

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

Assignment doesn't have transitivity. #42479

Closed heroboy closed 3 years ago

heroboy commented 3 years ago

Suggestion

🔍 Search Terms

extends conditional constrain

✅ Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

short example:

declare function SampleComponent(props: { a?: any; }, context?: any): React.ReactElement<any, any> | null;
declare var Tag1: React.FunctionComponent<{ a?: any; }>;
declare var Tag2: React.FunctionComponent<{}>;
declare var Tag3: typeof SampleComponent;

//Tag1 and Tag3 can assign to each other.
Tag1 = Tag3;
Tag3 = Tag1;

//Tag1 and Tag2 can assign to each other.
Tag1 = Tag2;
Tag2 = Tag1;

//But Tag2 and Tag3 can not assign to each other.
Tag2 = Tag3; //error
Tag3 = Tag2; //error

playground

📃 Motivating Example

I want to write a function to take any function component that can accept empty props.

function myrender<T extends {}>(Tag: React.FunctionComponent<T>) {
    return <Tag /> //error
}

function myrender2(Tag:React.FunctionComponent<{}>)
{
    return <Tag/>
}
myrender2(Tag3); //error

💻 Use Cases

jcalz commented 3 years ago

I think other issues of assignment transitivity have been raised and closed as design limitations, like #13043 and #40529. What, specifically, is the suggestion here? In the following code, what specifically should be changed?

declare var foo: { a: string, b?: number };
declare var bar: { a: string };
declare var baz: { a: string, b?: boolean };

foo = bar; // okay
bar = foo; // okay

bar = baz; // okay
baz = bar; // okay

foo = baz; // error
baz = foo; // error

The mutual assignability of foobar and barbaz is unsound but very convenient, and breaking it would break all sorts of real-world code. Enforcing transitivity here would imply that we should allow foobaz, but who wants that? Is there something better than what TypeScript already does here?

heroboy commented 3 years ago

@jcalz ok , I agree with you. Thanks.

masaeedu commented 2 years ago

Is there something better than what TypeScript already does here?

IMO something better would be to distinguish between closed records, which do not admit additional fields, and open records, which can have an arbitrary set of fields besides the listed ones. I'm not sure if there's an issue for that.

This kind of unsoundness is a pretty deep issue, and shows up in the bajillions of keyof related issues for example.

jcalz commented 2 years ago

"Closed records" would probably be "Exact types" as requested in #12936

fatcerberus commented 2 years ago

This kind of unsoundness is a pretty deep issue, and shows up in the bajillions of keyof related issues for example.

Unsound or not, it’s a fundamental part of the type system and completely by design: TS calls it structural subtyping, and it’s done that way because that’s what best models idiomatic JS. Soundness comes second. (in fact soundness is explicitly called out as not a primary pursuit in the design goals)

All that being said, #12936 is indeed rather popular as far as feature requests go; people often get to wanting it before they’re even able to articulate it. 😆

masaeedu commented 2 years ago

@fatcerberus The whole point is that it isn't what "best models idiomatic JS": it is idiomatic in JS to reflect over the keys of a record and produce runtime evidence of them, and the type system produces misleading and unhelpful information when one attempts to use this pattern.

A more fine grained distinction between records not known to have additional keys and records known not to have additional keys would facilitate safe use of idiomatic JS patterns like duck typing and key enumeration.