Typescript: Structural Subtyping with Control flow based type analysis

Created on 1 Aug 2016  路  7Comments  路  Source: microsoft/TypeScript

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,
    });
}
Design Limitation Suggestion

Most helpful comment

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 馃檮

All 7 comments

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"

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'.

@RyanCavanaugh

from here on out, config is now of type ConfigFixed

duplicate of https://github.com/Microsoft/TypeScript/issues/8545

from here on out, config is now of type ConfigFixed

would also be an asnwer to mixins or partial classes

https://github.com/Microsoft/TypeScript/issues/563#issuecomment-218841071

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 馃檮

@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 :grin:.

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...

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?
}
Was this page helpful?
0 / 5 - 0 ratings