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

Suggestion: explicit type narrowing #9946

Closed
saschanaz opened this issue Jul 26, 2016 · 12 comments
Closed

Suggestion: explicit type narrowing #9946

saschanaz opened this issue Jul 26, 2016 · 12 comments
Labels
Duplicate An existing issue was already created Suggestion An idea for TypeScript

Comments

@saschanaz
Copy link
Contributor

saschanaz commented Jul 26, 2016

To fix #5930 and #6474:

Syntax and use cases

let anything: any;
if (isFoo(anything)) {
  declare anything: Foo;
  anything.foo // valid and type-checkable
}

interface Foo {
  foo: number;
}

function isFoo(something: any): something is Foo {
    return typeof something.foo === "number"; 
}
let node: Node
if (node.nodeType === 1) {
   declare node: Element;
   node.addEventListener // valid
}
// Added on August 2, 2016
// Dog|House case from #9999
function fn(x: any) {
  if (x instanceof Animal) {
    declare x: Dog; // User knows better than the type system, so let it go explicitly
    x.woof(); // type-checked
  } else {
    declare x: House;
    // handle House case
  }
}
// Added on August 8, 2016
try {
  foo();
}
catch (err) {
  // when I'm absolutely sure that my error object will always be an instance of Error
  declare err: Error;
  console.error(err.messge) // compiler warns
}

Behavior

{
  declare anything: Foo;
  // `anything` is Foo in this block.
  // if type of `anything` is not narrow-able into Foo, it should be an error.
}

Why not existing type guards?

  1. any type cannot be narrowed by type guards. (User defined type guard function and type any #5930)
  2. Defining a new super-small function only for a single or two uses is not always preferable. (Allow inline type predicates #6474)

Workaround

Declare a new variable. var element = node as Element

@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Jul 26, 2016
@vilicvane
Copy link
Contributor

vilicvane commented Jul 26, 2016

+1 for this feature. I was also thinking of another way to have "type assertion type" assertion work in if statement (oops, just found that it is the same as what issue #6474 mentioned a long time ago, and referenced by SaschaNaz at the first place):

interface Yo {
    type: 'yo';
}

let foo: any;

if ((foo.type === 'yo') as foo is Yo) {
    // Now `foo` has type `Yo`.
}

@zpdDG4gta8XKpMCd
Copy link

the example with Node to Element is exactly what type guards were designed for

function isElement(node: Node): node is Element { return node.nodeType === 1; }

what is wrong with defining a super-small function for it?

i think that using declare node: Element; undermines:

  • type-safety: node.nodeType === 1 and declared node: Element not bound together and can easily go out of sync during refactoring
  • the purpose of type guards

@saschanaz
Copy link
Contributor Author

saschanaz commented Jul 26, 2016

type-safety: node.nodeType === 1 and declared node: Element not bound together and can easily go out of sync during refactoring

I think the unboundedness can help sometimes with its flexibility. What about this:

// jQuery ajax
$.ajax({
  url: "http://example.com/foo.xml",
  dataType: "xml"
}).then(data => {
  // data is any
  declare data: XMLDocument; // I know better than the type system!
});

PS: Specifically, I posted this suggestion because I had to catch an error and get error type.

try {
  /* ... */
}
catch (err) {
    if (isWebIDLParseError(err)) {
        const werr = err as WebIDL2.WebIDLParseError; // type narrowing does not work :(
        console.warn(`A syntax error has found in a WebIDL code line ${werr.line} from ${result.url}:\n${werr.input}\n`);
    }
    else {
        throw new Error(`An error occured while converting WebIDL from ${result.url}: ${err.message || err}`);;
    }
}

@zpdDG4gta8XKpMCd
Copy link

first: narrowing from any is one of my favorite dreams to dream of, so i am with you on that

the ajax problem isn't a problem as long as you develop a contract between input and output:

interface Request<Data> {
   url: string;
   dataType: string;
}

function fetchData<Data>(request: Request<Data>) : Promise<Data> {
   return $ajax(request);
}

const request: Request<XMLDocument> = {
   url: "http://example.com/foo.xml",
   dataType: "xml"
};

fetchData(request).then(xml => { /* xml is XMLDocument */ })

@yortus
Copy link
Contributor

yortus commented Jul 27, 2016

I posted this suggestion because I had to catch an error and get error type.

@saschanaz see also #8677

narrowing from any is one of my favorite dreams to dream of

@Aleksey-Bykov amen, same here. The compiler treats any annotations as meaning "I don't want any type checking on this variable". But what I often wish to convey to the compiler is "this variable could be anything at this point. I can't make assumptions until I narrow it", for which any also seems like the clearest and most readable annotation, but isn't right. I think {} works best in this case but I find it has unclear intent when I later re-read the code.

For me the best of both world would be that any still means "don't type-check this", but if I explicitly put it through a type guard then I'm saying "please do narrow this now since I'm explicitly asking you to". This is also more consistent with runtime semantics.

After all, an explicit type cast does work on any, so why not an explicit type guard?

But it's not going to happen because it's too much of a breaking change to make people change the annotation of x in the following example from any to Dog|House, which would make it work perfectly.

// By contract, this function accepts only a Dog or a House
function fn(x: any) {
  // For whatever reason, user is checking against a base class rather
  // than the most-specific class. Happens a lot with e.g. HTMLElement
  if (x instanceof Animal) {
    x.woof(); // Disallowed if x: Animal
  } else {
    // handle House case
  }
}

If TypeScript accepted a breaking change here with the advice to use a union type annotation rather than any in examples like this, I not sure if there would be any other objection to narrowing from any.

we want type guards to allow you to do more, not less.

...then let them work with any

any can already do everything

...except be narrowed even when explicitly requested

@zpdDG4gta8XKpMCd
Copy link

@yortus i've ranted a bit right past that example you mentioned, i have a strong opinion about the shady business that is going on there

@yortus
Copy link
Contributor

yortus commented Jul 27, 2016

@Aleksey-Bykov your rant is spot on. Shady business indeed.

@zpdDG4gta8XKpMCd
Copy link

related #9999

@saschanaz
Copy link
Contributor Author

saschanaz commented Jul 29, 2016

( @rozzzly deleted post by mistake, original post: #9946 (comment) )

Great, and I think this can be better for boundedness sake.

function foo(x: any) { 
  // type of x is `any`
  if (someCondition): x is number[] { // doesn't matter what this is
    // now type of x is `number[]`
    x.push (7);
  }
  // type of x is `any`
}

@rozzzly
Copy link

rozzzly commented Jul 29, 2016

I accidentally deleted my post prior to saschanaz's because the buttons on my phone are so small. 😡 This is what it said:

I've always wanted something like

function foo(x: any) { 
    // type of x is `any`
    if (someCondition) { // doesn't matter what this is
        // type of x `any`
        x is number[];
        // type of x is `number[]`
        x.push(7);
    }
    // type of x is `any`
}

I think this would be a good fit! It's simple and looks/behaves like a type guard. It seems like a natural extension of the concept. Unfortunatly, is is not a reserved keyword but I doubt many people name their variables "is", but throw it behind a flag if you're worried about breaking changes. I would imagine however that the lexer could be made to differentiate a type assertion of this style from other code.


@saschanaz ooh that's nice too, definitely helps by defining the scope within which that type has been narrowed. But, I think adding it to an if could be somewhat restrictive because it has to be accompanied by an if. I can't think of any practical examples where that would be an issue off the top of my head though :/

oh and what about this...

function foo(x: string | number) { 
    // type of x is `string | number`
    if (someCondition): x is string {
        console.log(x.toUpperCase()); // valid because we know x is a string
    } else { 
        // typeof x is `string | number`
        // someone could get confused here and think that x is a
        // `number` like you can do with type guards on unions
        // but the condition very well might depend on something 
        // other that the type of x being a string for example 
        if (true === false && _.isString(x)): x is string {
             // will never occur because true !== false
        } else {
             // x could still be a string.
        }
    }
}

@saschanaz
Copy link
Contributor Author

oh and what about this...

That problem also occurs with type guard functions.

function isString(something: any): something is string {
  return true === false && _.isString(x)); // ???
}

@RyanCavanaugh
Copy link
Member

Let's track this at #10421

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants