microsoft/TypeScript

"Stricter" TypeScript

RyanCavanaugh opened this issue ยท 40 comments

This a meta-bug for tracking a set of things that we could address with a compiler flag that tightens certain aspects of TypeScript that users generally perceive as too loose.

Will add to this list as appropriate

  • #228: Enforce expressions in return statements in type-annotated functions
  • #191: Don't allow mismatched constant string overloads to be selected due to other parameters
  • #21: Disallow use before definition (for types? for everything?)
  • #360: Error when generic type inference produces ex nihilo {} - now a default

Contentious issues that have been mentioned:

Done!

  • #391: Detecting accidental surplus properties

Not happening:

  • #222: Function argument bivariance
    • We would want some strong motivating examples here; this behavior is desired in a lot of scenarios and is less "surprising" than others.
    • This isn't a plausible design path. See comments here #6102 (comment)

Note: This is not a "fork the language" flag. The type system itself would be unchanged under this flag; it would simply change some operations from being allowed to being errors. For example, we would not change the order of overload resolution, or change the type of null, because that would have non-local effects and everyone would have to agree on whether or not the flag was on.

Does it make sense for noImplicitAny to be rolled in as part of this stricter TypeScript? Or is there value keeping it separate?

I'd like to hear feedback on that one -- would anyone want --strict without --noImplicitAny ?

I think --strict should include --noImplicitAny.

I think --strict should include --noImplicitAny.

๐Ÿ‘

It'd be nice to have a flag to tighten the type compatibility rules (e.g. #222)

I think --strict should include --noImplicitAny. ๐Ÿ‘

This is a great meta-issue. My preference is for all these features (including noImplicitAny) to be included in the default compilation (i.e. no flags).

We could have a flag that people can use to relax the rules, for example --lenient or --dontCatchErrors (that's a joke, btw).

A --strict flag might be confused with "use strict".

A --strict flag might be confused with "use strict".

that is what I thought when I first looked at the title

Yeah it may be the case that we need to use a different term than 'strict' to minimize confusion but it's ultimately a small issue that's easy to change at any time before the feature goes into a final release.

@NoelAbrahams - it'd need to be behind a flag to not break backward compatibility with code that's building against the current 1.0 compiler.

Not that I want to start a woodshed about naming, but I ๐Ÿ‘ that "strict" is too overloaded, especially in the JS world.

It's too late to make noImplicitAny the default; this would break too many people.

I agree with the sentiment that strict is overloaded in JS and is probably not the best name for the flag should it be implemented. Ideas on that front?

It's too late to make noImplicitAny the default; this would break too many people.

๐Ÿ‘

Ideas on that front?

--safer or --loud

How about --hardcore or --optionExplicit? ๐Ÿ˜„

In all seriousness I think --safe works well as would --rigorous. Both communicate that the compiler should be more exacting but both words have positive connotations (which invite usage rather than forbid it).

--ultraprecise

I also like --safer (or simply --safe)

--safe sounds good, I also like --warn:<n> flag of the C# compiler to adjust the warning level for the compiler.

Perhaps the important question here is not so much the name but the compatibility promise associated with the flag. Will it only enforce a certain set of restrictions forevermore or can new restrictions be added under the same flag in future?

My personal preference would be to enable new restrictions by default under such a flag but provide opt-outs on a per-module basis that can be easily enabled to keep existing code compiling under new versions of tsc. The rationale being that it will improve the quality of the code in the TypeScript ecosystem as a whole.

I'd like to see --noImplicitAny made the default but that would break the important feature that new users can convert existing JS code to TypeScript just by changing the file extension.

--warn:<n> seems like a nice idea. It's a bit more future proof, and deals with @robertknight's comment about adding new restrictions.

  • warn:0 - The default. Same as compiling without noimplicitany.
  • warn:1 - Adds --noimplicitany.
  • warn:2 - Disallow use before definition, etc.
    ...
  • warn:N - A future requirement

C# compiler to adjust the warning level for the compiler.

@KamyarNazeri Is the error code from the compiler non-zero (e.g. 1) on warnings?

The warning level for C# compiler is a zero-index value, 0 being the lowest (turns off emission of all warning messages) while higher numbers show more warnings:
http://msdn.microsoft.com/en-us/library/13b90fz7.aspx

As mentioned by others, I too believe having levels of warnings could be extensible for future use

Is the error code from the compiler non-zero (e.g. 1) on warnings?

The answer was on the page @KamyarNazeri mentioned for C#:

Use /warnaserror to treat all warnings as errors.

Since we want all of these to be errors perhaps we should use --error:<n> or we would need --warnAsError as well

Our general plan is to provide simpler flags (probably just --noImplicitAny and --stricter or whatever we would call it) for tsc and let third-party build tools provide fancier stuff. Since everything will have an error code, it will be straightforward for tools like grunt-ts to provide filtering on the errors provided by the compiler.

Not sure if this has been mentioned before, but how about an option to restrict multiple uses of interface declarations? I've run into a case where I forgot that I had already used an interface name somewhere and in this case the 2 interfaces get merged - which I understand is useful - but in some cases it would be useful to be able to say that you want this interface to not collide with any other one, so in case another exists already, throw a compile error and let you know that you need to pick another name?
Maybe with something like:

unique interface IMyInterface {}

This is a tracking issue for other issue, all of which are in the category of changes to the type system rather than additional features or syntax. Please post suggestions for new syntax or features elsewhere. Thanks!

perhaps this suggestion #185 could be included in the list

Again, this is only for additional "rules", not new features or new syntax.

I came here after discovering hidden bugs in my code while, which wouldn't have slipped through if TypeScript had said list of stricter features, so I'm very supportive of this idea.

But I want to ask the TypeScript team to consider making this mode (and --noImplicitAny) be possible to specify on a file-by-file basis, so we can actually use it.

The majority of real-world projects reference third party definitions and libraries, which are typically not coded to take into account features like --noImplicitAny. Using this flag project-wide results in hundreds of errors piling up in our IDEs, and makes it very impractical to keep the flags enabled, even though we want to use them.

It should be possible to implement a simple syntax to enable this only for files where we need this (ex. our files, not third party files), much like JavaScript's "use strict" pragma.

Note #1266 as a potential new rule for only allowing addition and similar operators to work on types that 'make sense.' This this case adding functions does give a somewhat reasonable result (allowing you to inspect the name, args, and body) but that may or may not be a net win for some, and there're other types that certainly give useless output ([Object object]).

Note #1740 as a potential additional error level, although it may be too much of the 'fork the language' case and there're better solutions if we do real 'this' typing in the core language.

I think it would be really nice if I could ask the compiler to warn me when I do this:

var data:Model = JSON.parse(json);

Instead of "silently" assigning the value of type any to data:Model, I would want the compiler to complain.

The compiler would stop complaining if I provided a type assertion. For example:

var data:Model = <Model>JSON.parse(json);

I like this because I am assuming responsibility for the type cast. If json doesn't represent data that conforms to the Model interface, that's my fault.

Thoughts?

You can get the desired behavior by changing the return type of JSON.parse to {}. Disallowing all uses of any in its intended form seems far too strict.

Since you're looking for motivating examples for disallowing function argument bivariance: we have code where we pass around constructor functions. These constructors accept an argument of an interface, but one of them incorrectly assumed it would get one specific concrete implementation of this interface instead. We expected this kind of thing to be caught but instead it was a runtime bug.

It would be good to have ability to raise warning/error for all properties which don't have โ€ƒโ€ƒAccessibilityModifier like private | public | protected in the stricter mode.

@markbook2 you can do that easily today with a regular expression( see below).
Edit: But use tslint.

Plus, this is a meta-bug, so you should probably file a new issue and reference this.

@markbook2 @afrische TSLint has a rule for enforcing explicit visibility on class members / methods called member-access

I think --strict should include --noImplicitAny.

๐Ÿ‘ --noImplicitAny should be default, but obviously for backcompat reasons it can't be. But it should definitely be part of any '--strict' mode.

Here is another motivating examples for disallowing function argument bivariance:

In our typescript code base, we have an interface for classes having an equals method

export interface Eq {
    equals(x: any): boolean;
}

Because of function argument bivariance, classes can implement this interface "incorrectly", that is by narrowing the argument type any. For example:

class C implements Eq {
    constructor(private c: number) {}
    // I would like to get an error here, stating that the method signature in 
    // the Eq interface does not match the implementation given here. 
    // In OO-languages such as C# or Java, you get this error.
    equals(that: C) {
        return this.getNum() === that.getNum();
    }
    getNum() {
        return this.c;
    }
}

class D {
    constructor(private d: number) {}
    equals(that: D) {
        return this.d === that.d;
    }
}

We now write a function that searches in an array of Eq-objects for an element:

function findInArray(arr: Array<Eq>, x: Eq): number {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].equals(x)) {
            return i;
        }
    }
    return -1;
}

Now it's rather easy to trigger an unexpected runtime error:

const arr = [new C(1), new C(2)];
const i = findInArray(arr, new D(2));
console.log(i);

The program now aborts with TypeError: undefined is not a function

We have moved to a more piecemeal approach to such checks, e.g. control flow checks, strict this, strict null, etc.. feels like this issue is obsolete. @RyanCavanaugh any objections to closing it?

Another case where bivariant arguments really hurt is the React and Redux world, where mismatches in React component props are one of the most frequent source of errors.

Currently it's perfectly valid to have:

interface MyComponentProps {
  title: string;
}
export class MyComponent extends React.Component<MyComponentProps, void> {
  // code that needs this.props.title
}
const MyComponentAlias: React.ComponentClass<{}> = MyComponent;
// use of MyComponentAlias blows up, as it doesn't require title, but title is needed for MyComponent

The const in the above example is not a common use case, but I'm currently trying to make react-redux (especially the connect function) typings stricter, so props interface changes would highlight other changes that need to be made, but the bivariance only allows checking that the property field types are right, not that all properties are present where they need to be (and therefore also allows typos).

I would like a flag under which TypeScript would reject any program that it could not prove to be free of type errors at runtime.

In addition to proving that a large class of bugs (type errors) are impossible, an AOT compiler (for servers) for "sound" typescript could erase the types at runtime for a perf gain.

@DemiMarie what you're describing is not broadly possible given the constraints of our type system. As one example of many, we have no plausible way to prevent mutation after aliasing a structure with a less-specific type introducing an incorrect value into that structure, e.g. :

const a = [1, 2, 3];
const b: Array<string | number> = a;
b[0] = 'oops';
console.log(a[0] + 1); // 'oops1'

We've done a good job implementing almost everything in the OP so I'm going to close this due to a lack of usefulness of a meta-issue. Some good discussion happening at #9642 about how to get these stricter options on by default in places where it wouldn't be troublesome.