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

Structural Subtyping with Control flow based type analysis #10065

Closed
vvakame opened this issue Aug 1, 2016 · 7 comments
Closed

Structural Subtyping with Control flow based type analysis #10065

vvakame opened this issue Aug 1, 2016 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Suggestion An idea for TypeScript

Comments

@vvakame
Copy link
Contributor

vvakame commented Aug 1, 2016

I like --strictNullChecks options.
But this options is very hard to apply existing project.
I want to remove optional operator from interface.
TypeScript 2.0.0 can it! but assignment operation is not under control flow context.
I think we need it.

for example.

declare var fs: { readFileSync(filePath: string, encoding: string): string; };

interface Config {
    filePath?: string;
    verbose?: boolean;
}

// add for --strictNullChecks support. I want use this interface wider.
interface ConfigFixed {
    filePath: string;
    verbose: boolean;
}

function foo(config: Config = {}) {
    config.filePath = config.filePath || "settings.json";
    config.verbose = config.verbose || false;

    // config.filePath is string. ts 2.0.0 is super cool!
    config.filePath;
    // config.verbose is boolean. ts 2.0.0 is very smart!
    config.verbose;

    // but an error occured.
    // test.ts(16,9): error TS2345: Argument of type 'Config' is not assignable to parameter of type 'ConfigFixed'.
    //   Types of property 'filePath' are incompatible.
    //     Type 'string | undefined' is not assignable to type 'string'.
    //       Type 'undefined' is not assignable to type 'string'.
    bar(config);
}

function bar(config: ConfigFixed /* replace from Config since TypeScript 2.0.0 */) {
    let data = fs.readFileSync(config.filePath, "utf8");
    config.verbose && console.log(data);
}

or

function withDefaultValue(config: Config = {}): ConfigFixed {
    config.filePath = config.filePath || "settings.json";
    config.verbose = config.verbose || false;

    return config;
}

function buzz(config: Config = {}) {
    // I can add below line without constraint.
    config = withDefaultValue(config);
    // I want to `config` value be `ConfigFixed` type by Control flow based type analysis.
    config.filePath;
}

Off topic.

function withDefaultValue2(config: Config = {}): ConfigFixed {
    // valid. cool!
    return Object.assign(config, {
        filePath: "settings.json",
        verbose: false,
    });
}
@RyanCavanaugh
Copy link
Member

Continuing to think about this but it'd be very expensive -- we'd be resynthesizing a type for config after every assignment. It's more likely we'll figure out some kind of scoped assertion syntax that lets you specify "from here on out, config is now of type ConfigFixed"

@zpdDG4gta8XKpMCd
Copy link

migrated from #12919

allow an object narrowed to a certain interface be used where such interface is expected

interface A { value: number | string };
interface B { value: number; }
function takeOnlyB(value: B): void {}
const data: A = { value: 'hey' };
data.value = 1; // <-- from this point on we know for sure that value is number
takeOnlyB(data);  // <-- problem, despite effectively being of type `B` data still can't be used

// expected:  takeOnlyB should accept data because it's been asserted to comply with `B`
Argument of type 'A' is not assignable to parameter of type 'B'.
  Types of property 'value' are incompatible.
    Type 'string | number' is not assignable to type 'number'.
      Type 'string' is not assignable to type 'number'.

@zpdDG4gta8XKpMCd
Copy link

@RyanCavanaugh

from here on out, config is now of type ConfigFixed

duplicate of #8545

@zpdDG4gta8XKpMCd
Copy link

from here on out, config is now of type ConfigFixed

would also be an asnwer to mixins or partial classes

#563 (comment)

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 7, 2017

A casual observation is that allowing this would be unsound because someone could hold a reference to the guarded object, then have the object mutate at a later point during execution to have an incorrectly-typed property (from the perspective of the callee). We're in general very unsound in this regard already so it's not really a reason to not do the feature... but people are always telling me TypeScript is Very Bad because it's unsound so I figured it was worth mentioning in the context of this suggestion 🙄

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Sep 8, 2017

@RyanCavanaugh

people are always telling me TypeScript is Very Bad because it's unsound so I figured it was worth mentioning in the context of this suggestion

Unsound? Yes.
Very bad? Not on your life 😁.

In all seriousness, it works for people, lets us get our jobs done, and follows its stated goals.

The rough edges, which are getting smoother every day, come more from things like module resolution, declaration acquisition, and bad introductory materials (see Angular tutorials), that give developers the wrong impressions about the purpose of the language. Even the Wikipedia page states that TypeScript brings class-based object orientation to JavaScript, so there's a lot of incorrect information out there and when people want to believe something, they find a way...

@jkillian
Copy link

jkillian commented Sep 19, 2017

Ran into essentially the same scenario today with narrowing union types. Key difference here is that in my case there's no assignment happening, only type guards. If this is a different enough, happy to break it out into its own issue if necessary.

type Union = A | B;
type A = { type: "a", isA: boolean };
type B = { type: "b", isB: boolean };

interface Container { union: Union };
interface ContainerNarrowed extends Container {
    union: A;
}

declare const container: Container;

if (container.union.type === "a") {
    // this is valid, so TS knows that `union` is an `A`
    container.union.isA;

    // compiler error:
    // Type 'Container' is not assignable to type 'ContainerNarrowed'.
    //   Types of property 'union' are incompatible.
    //     Type 'Union' is not assignable to type 'A'.
    const containerNarrowed: ContainerNarrowed = container;
    // it seems as if `container` should satisfy `ContainerNarrowed` here?
}

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

No branches or pull requests

5 participants