-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Strict variance and read-only versions of types #18770
Comments
Here is a rough sketch to what we have discussed over the past years regarding variance checking. if we were to implement it we need at least:
|
@mhegazy If I understand correctly, does the second suggestion cover function calls that may currently modify readonly types? In particular, got bitten before by popular "immutable" |
I think we can do that regardless of variance. the issue here is we do not check type I = { p: number };
declare var i: I;
declare var roi: Readonly<I>;
i = roi; // Not an error!
roi.p = 0; // Error, p is readonly This was tracked by #11481. The reason why we have shied from this check is it forces users to mark their parameters as readonly consistently everywhere once one of their libraries adopts a stricter readonly annotations. This might have been overblown concern, and maybe we should revisit our decision of a new |
Hmm, true. But maybe TS could provide a simplified migration path for that concern, automatically "upgrading" any parameter types to readonly whenever it knows that function body doesn't modify the object? Then most of the code would be still compatible across such changes. |
that is a possible solution. we have talked about code mods in the past.. |
I thought rather about runtime type detection than codemod, but codemod would work as well. |
Basically: // only reads `obj.prop`, so TS changes type of `f` to `({ readonly prop: number }) => ...`
function f(obj: { prop: number }) {
return obj.prop;
} |
I'm the maintainer of wix-react-tools type One = { one: 1 };
type OneTwo = { one: 1, two: 2 };
declare const useCase: (f: (f:One) => void) => void;
declare const fooOneTwo: (f:OneTwo) => void;
// this fails contravariance
useCase(fooOneTwo);
/* Error: TS2345:Argument of type '(f: OneTwo) => void' is not assignable to parameter of type '(f: One) => void'.
Types of parameters 'f' and 'f' are incompatible.
Type 'One' is not assignable to type 'OneTwo'.
Property 'two' is missing in type 'One'.
*/ This is a straightforward example for the new contravariance check introduced in 2.6, as it protects the user from a scenario where function useCase(f: (f:One) => void){
f({one:1});
} However, in our case, the Up until now We've given the users a type-safe experience, with full support in strict-mode checks. To support typescript 2.6 (wix-incubator/wix-react-tools#188) We now need to choose between asking them to use In all the proposals regarding contravariance checks there was some sort of in/out notation that would have allowed us to avoid this dilemma. I'm very much interested in any plans of supporting such notations. |
Then shouldn't declare const useCase: <P extends One>(f: (f:P) => void) => (f:P) => void; |
@mattmccutchen That could be a workaround , but I don't get to define type Feature<T extends object> = <T1 extends T>(subj: T1) => T1;
type ReactFeature<P extends object> = Feature<ComponentType<P>>;
declare const useCase: ReactFeature<One>
@useCase // error
class X extends React.Component<OneTwo> {}
// error
const Y = useCase((p:OneTwo)=>{...}) (see original code here, here, here and here) type ReactFeature<P extends object> = Feature<ComponentType<extends P>>; |
@amir-arad Now that I learned the basics of React for unrelated reasons, I took another look at this and didn't see an obvious solution, though I still don't fully understand what you're trying to do. I don't think discussing your case further in this thread is helpful; I suggest you start a new thread on one of the normal support channels. Linking to the original code is nice, but to make it easy for people to help you or evaluate a suggestion, you'll probably need to provide a brief explanation of why each type is set up the way it is and what the decorator actually does. |
A possible application of that The result type of However, typescript thinks that, as this object looks like an input, that the A concrete example: const htmlRef = React.createRef<HTMLElement>();
// type error; HTMLElement not assignable to HTMLDivElement
<div ref={htmlRef}/>; However, the assignability is supposed to apply the other way around. This works with function refs because of the |
I'd like to suggest a solution here: for a lot of the javascript code I work with, the vast majority does not mutate javascript objects in place, and when it does so, it's mutating locally created objects rather than function arguments. This is particularly the case with react and other functional/reactive frameworks. The good thing about Flow is that it (somewhat) correctly type-checks mutable vs readonly types, by making mutable types invariant, whereas Typescript is currently unsound in this regard, in order to be easier to use. The reason Flow is hard to use comes from a few reasons:
If Typescript is going to add a
Example of why it is important to be able to construct a deeply immutable version of a type, and readonly is not sufficient (neither Typescript nor Flow have any "good" way of soundly type-checking this code without introducing a lot of intermediate types): type Foo = {
x: { y: number | undefined }
}
// Refine "Foo" for when we know "y" is present
type Bar = Foo & {
x: { y: number }
}
function takesABar1(bar: Readonly<Bar>) {
...
}
function example1(foo: Readonly<Foo>) {
if (foo.x.y !== undefined) {
// This should not compile, because even though we've checked the constraint,
// there's nothing stopping the constraint from being invalidated at a later time
// by the caller, who has mutable access to the Foo
takesABar1(foo);
}
}
function takesABar2(bar: Immutable<Bar>) {
...
}
function example2(foo: Immutable<Foo>) {
if (foo.x.y !== undefined) {
// This is completely safe, because we are guaranteed by the caller that
// "foo" will not be changed, and even that "foo.x" will not be changed.
takesABar1(foo);
}
} Here I am manually annotating types as being
The third option would be the hardest, but would also be the least breaking, as much code will just continue to work even with the new flag enabled. The compiler will need to perform some kind of escape analysis to allow going from mutable to immutable values. Example: function test(arg: Immutable<Array<number>>) { ... }
const x: Array<number> = []; // New array, compiler can see we have the only reference
x.push(4); // Compiler infers that in this line and the line above, the array should be mutable
test(x); // OK, because we have the only reference to `x` so we can "convert it" without copying
x.push(3); // Not OK, "test" could have kept a reference to `x`, and we guaranteed that it would be immutable Read-only support would still be useful in addition to immutability. |
Hello everyone, Just thought I'd drop this here, It's a prototype typescript-eslint rule that attempts to make all mutable properties invariant. I've tested it on some projects at work and it's not 100% accurate but it does the job for the most part. While waiting for full typescript support, this might be a good interim solution. There's no npm package yet because this is still a prototype. You can make it a custom rule for your projects, though. One of the projects has thousands of files and running the rule lead to fixing about 10 unsound assignments. There was one problem that was with an external library. Luckily, it was a company internal library and we only had to fix one parameter's type to The other problem is that this is considered unsound, declare const src : Buffer;
//Lint error
const dst : UInt8Array = src; This is considered unsound even though So, it is possible to so this, dst.indexOf = someImplThatDoesNotHandleBufferOverloads;
src.indexOf(/*Buffer specific arguments*/); //Explosion, the impl does not handle Buffer overloads This situation is highly unlikely, given that people usually do not replace method implementations like this. So, I'll probably add a config option that checks if the source type explicitly [Edit] It now checks Actually, never mind. The whole type checking thing is way above what my brain can handle. And ts-simple-type gets many simple cases right but falls flat on the not-so-simple ones. Can't blame it, though. It essentially has to rewrite all of TS' checking code if it wants to emulate it perfectly. |
I'm curious if anyone's run into actual bugs in their code from the lack of strict variance checking on assignments and function calls where the source and params are variables. |
It's happened to me a bunch of times when I used to write OOP-y code with a lot of state mutations in TS. I don't do that so much anymore. I mostly treat objects as immutable nowadays. This bypasses a lot of variance problems. If you look at the table here, #33234 You'll see that we currently don't have write-only properties, so we can ignore that row. Readonly properties are considered covariant at the moment and this is sound. (Most of the time. The readonly-to-read-and-write assignment is allowed and that is unsound) The read-and-write properties are the problem here. They're still considered covariant when they should be considered invariant. You can see an example on #33234 for why covariant read-and-write properties are unsound. However, if you start treating everything as immutable, even read-and-write properties, you've essentially also eliminated that row from the table in #33234 as well. All that is left are readonly properties in the table and those are sound. So, by pretending everything is immutable, you side step many problems with variance. It does make me a little nervous when I have to step back into the "mutating world" though, because I know the type system won't catch me if I trip up |
So if we had a linter to ensure that all objects and arrays were immutable then we'd be safe. That's probably something worth exploring. |
In terms of linting for immutable arrays and objects I've been using https://github.com/jonaskello/tslint-immutable which I'm very happy with. It seems like they are migrating it to ESLint with https://github.com/jonaskello/eslint-plugin-functional. |
I've recently started a project to explore working with mobx and ran into a bug when working with a copy constructor in a child class. Took me a time to figure out that I must treat fields as invariant. See #35006 |
I have another use case for code below works without compiler error: interface I {
readonly a: string
}
class C implements I{
a= ""
}
const D = new C
D.a = "something" in order to make property 'a' really readonly I should make it readonly in class definition too! in other word how can I make sure when I am creating a class by implementing interface I am creating it with right modifier? |
I also ran against this another time. My current use case is to identify several components by giving them a name. To easily use these components I created a union type of them and wanted to map to another union type with only their names as string literal types in it for other purposes. E.g.: interface Component { readonly name: string }
class ComponentA implements Component { readonly name = 'a' }
class ComponentB implements Component { readonly name = 'b' }
export type Components = ComponentA | ComponentB
type GetComponentNames<T extends Component> = T["name"]
export type ComponentNames = GetComponentNames<Components> // "a" | "b" This works as long as the class ComponentC implements Component { name = 'c' }
export type ComponentNames2 = GetComponentNames<Components | ComponentC> // string Here is a link to a tsplayground for these examples. |
I feel like we should have an open "suggestion" issue with a meaningful title to track the work on stricter variance and read-only versions of types that the TypeScript team is considering, especially as other issues are being duped against it. The initial design sketch and a few comments are in #17502.
After a little more searching, should this be merged with #10725?
The text was updated successfully, but these errors were encountered: