-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Option strictMethodType
: strictFunctionType
for method syntax.
#57783
Comments
strictMethodType
: strictFunctionType
for method syntax.strictMethodType
: strictFunctionType
for method syntax.
Anyone who turns on such a flag would undoubtedly see lots of breakage. Arrays are (unsoundly but conveniently) covariant in TypeScript and lots of code depends on that. I love soundness but if I introduced a flag that broke everything when turned on, I don't think saying "it'll be off by default" would help much. This would be breaky. |
Particularly since, because of how TS works, enabling the flag for your own project probably requires any dependencies you have to successfully compile against it, even if they donβt need it themselves. With options like this, in a lot of ways theyβre only βoptionalβ in theory. |
I checked My guess is that this should be less of an issue now than it was 7 years ago when `strictFunctionType' was introduced.
Have you some practical examples? I am curious about that.
Still, it makes sense for many projects to have this option enabled.
I am unsure to follow you. This could be checked at usage site? Similarly to |
Assuming that enabling such an option doesn't break everything, having the option at least allows users to file issues and PRs on dependencies that don't work with it and work toward making the ecosystem compatible. |
So, you're presumably aware that interface Array<T> {
push(...items: T[]): number;
} And this would be contravariant with your proposed flag, making interface Animal { name: string }
function animalNames(animals: Animal[]) { return animals.map(x => x.name) }
const animals = [{ name: "Fido", age: 10 }, { name: "Snoopy", age: 5 }];
console.log(animalNames(animals));
// -------------------> ~~~~~~~ because even though every element of type DistributiveFoo<T> = T extends Bar ? Baz<T> : Qux;
type NonDistributiveFoo<T> = [T] extends [Bar] ? Baz<T> : Qux And that would stop working and break all the things. In the docs for
So it's not just
You apparently didn't get my point. Having the flag off by default doesn't mean it's not a breaking change. Anyone who wants to use your option would see breakage. Just having the option exist means that large amounts of TypeScript code (including the standard libraries provided with TypeScript itself) would need to change so that people who turn on the flag only see problems in their own code and not in a large chunk of the libraries they depend on. You're not proposing merely adding a flag; you're proposing adding a flag and rewriting a lot of TypeScript declarations. It really doesn't help if the only way for this not to be a breaking change is if the flag is off. We can do that right now by not introducing the flag. And again, I'm not saying "this is a bad suggestion", I'm saying "this would be a breaking change". TypeScript does sometimes make breaking changes. Even |
This actually does break everything; it would make it roughly impossible to obtain an array or similar "actually invariant" object. For example, you can't write const arr: number[] = []; because the RHS is This can't be fixed with contextual typing of literals since there are other cases like: // Correct program rejected --
// Invalid, RHS is number[]
const arr: unknown[] = [1, 2, 3].slice(); The underlying problem is there's no available distinction between these two methods: class ArrayHolder {
private arr: number[] = [];
// Safe to covariantly alias the return of this function
getCopy() {
return arr.slice();
}
// Not safe to covariantly alias the return of this function
getSelf() {
return arr;
}
} Both are just " For this flag to even be plausible, there'd need to be a reasonable way to solve this problem. |
Most of its issues could be solved by using However, I accept that this could involve large breaking changes. Another possibility could create an exception for arrays. This is what Java is doing by making arrays covariant. Given the current philosophy of TypeScript, I am no longer sure that this issue has any chance of being accepted. |
@jcalz i'd love to see the breakage in my code. i've carefully avoided bivariance where possible, but i don't feel confident at all that i didn't accidentally mess up somewhere the compiler didn't want to check for me. |
Your dependencies on bivariance are almost certainly implicit. Out of the gate, the core lib is broken: ../../github/TypeScript/built/local/lib.es2015.generator.d.ts:21:11 - error TS2430: Interface 'Generator<T, TReturn, TNext>' incorrectly extends interface 'Iterator<T, TReturn, TNext>'.
Types of property 'return' are incompatible.
Type '(value: TReturn) => IteratorResult<T, TReturn>' is not assignable to type '(value?: TReturn | undefined) => IteratorResult<T, TReturn>'.
Types of parameters 'value' and 'value' are incompatible.
Type 'TReturn | undefined' is not assignable to type 'TReturn'.
'TReturn' could be instantiated with an arbitrary type which could be unrelated to 'TReturn | undefined'.
21 interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
~~~~~~~~~
../../github/TypeScript/built/local/lib.es2018.asyncgenerator.d.ts:21:11 - error TS2430: Interface 'AsyncGenerator<T, TReturn, TNext>' incorrectly extends interface 'AsyncIterator<T, TReturn, TNext>'.
Types of property 'return' are incompatible.
Type '(value: TReturn | PromiseLike<TReturn>) => Promise<IteratorResult<T, TReturn>>' is not assignable to type '(value?: TReturn | PromiseLike<TReturn> | undefined) => Promise<IteratorResult<T, TReturn>>'.
Types of parameters 'value' and 'value' are incompatible.
Type 'TReturn | PromiseLike<TReturn> | undefined' is not assignable to type 'TReturn | PromiseLike<TReturn>'.
Type 'undefined' is not assignable to type 'TReturn | PromiseLike<TReturn>'.
21 interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> extends AsyncIterator<T, TReturn, TNext> {
~~~~~~~~~~~~~~
../../github/TypeScript/built/local/lib.es5.d.ts:331:11 - error TS2430: Interface 'CallableFunction' incorrectly extends interface 'Function'.
Types of property 'apply' are incompatible.
Type '{ <T, R>(this: (this: T) => R, thisArg: T): R; <T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args: A): R; }' is not assignable to type '(this: Function, thisArg: any, argArray?: any) => any'.
The 'this' types of each signature are incompatible.
Type 'Function' is not assignable to type '(this: any) => any'.
Type 'Function' provides no match for the signature '(this: any): any'.
331 interface CallableFunction extends Function {
~~~~~~~~~~~~~~~~
../../github/TypeScript/built/local/lib.es5.d.ts:368:11 - error TS2430: Interface 'NewableFunction' incorrectly extends interface 'Function'.
Types of property 'apply' are incompatible.
Type '{ <T>(this: new () => T, thisArg: T): void; <T, A extends any[]>(this: new (...args: A) => T, thisArg: T, args: A): void; }' is not assignable to type '(this: Function, thisArg: any, argArray?: any) => any'.
The 'this' types of each signature are incompatible.
Type 'Function' is not assignable to type 'new () => any'.
Type 'Function' provides no match for the signature 'new (): any'.
368 interface NewableFunction extends Function {
~~~~~~~~~~~~~~~ The DOM also has hundreds of errors because Events are not meant to be called from supertype aliases. It makes This also breaks correct assumptions about array variance, specifically for readonly arrays. This code is sound by inspection: interface MiniatureReadonlyArray<T> {
getMutableCopy(): MiniatureMutableArray<T>;
[i: number]: T;
}
interface MiniatureMutableArray<T> extends MiniatureReadonlyArray<T> {
push(arg: T): void;
}
declare const sn_mini: MiniatureReadonlyArray<string | number>;
// Fails (wrong)
let snb_mini: MiniatureReadonlyArray<string | number | boolean> = sn_mini; But it errors because from TS's perspective, a method returning an invariant type must make the entire type invariant. We'd need a number of fairly invasive scaffolding features to make this work palatably. As-is, "just" making methods covariant creates a lot of incorrect errors. You can try out this branch. https://github.com/RyanCavanaugh/TypeScript/tree/strictMethodTypes . It creates a new option "demoStrictMethodTypes" |
Alternatively we could require explicit notation o make a parameter/return type bivariant. interface I {
f(param: out Type): in boolean
} I proposed a similar mechanism for Eiffel ten years ago. |
Would it make sense to do this just for method syntax in object literals? |
Why is this supposed to be a correct program? It looks more like some kind of hack/workaround. Who would intentionally cast a known type to an unknown in a normal project? const arr: unknown[] = [1, 2, 3].slice() as unknown[] .
Yes, this code should fail. Firstly, a readonly array shoud have The other examples in this thread were incorrect due to the lack of readonly. I'd like to see a real problem with 'strictMethodType' in properly designed code. So far I only see weird examples that I wouldn't use. As for DOM, this library itself is bad, because it relies on global variables. This is far from safe programming anyway. So when using it, 'strictMethodType' option could be disabled. |
π Search Terms
β Viability Checklist
β Suggestion
While the TypeScript team has deprecated legacy options in 5.0 and is looking to deprecate other options, I would like to revisit the decision to exclude method signatures from `strictFunctionType'.
The TS community is now more used to the strict subset of TypeScript. And this is now the standard and recommended way to use TypeScript. StrictFunctionType brought a lot of security, but it didn't apply to methods because it broke too much code at the time.
The difference between function properties and methods is not clear to many users. The bivariance of methods can even be seen as a bug from a normal user's point of view. It is also more convenient to use the method syntax, because it is shorter and seems more idiomatic. For these reasons, users don't get the security of `strictFunctionType' when writing interfaces, classes or object types.
I think it is time to introduce a new option
strictMethodType
, which applies strict variance to methods.This could also be the opportunity to make method readonly by default under this new option.
To ensure backwards compatibility, the option could be disabled by default for the time being.
π Motivating Example
π» Use Cases
I want to get the safety benefit of
strictFunctionType
without loosing readability when writing interfaces and classes.The text was updated successfully, but these errors were encountered: