Skip to content

Sane alternative to typeof | is | as | instanceof ? #28337

Closed

Description

Search Terms

typeguard type guard is instanceof instance of typeof typedefof as

Suggestion

All current type guards supported by TypeScript are substandard, full of "gotchas", and especially: extremely confusing for C# developers.

Use Cases

Problems with typeof: it's not strongly typed. It returns a string. Why not return a typed object so that you can compare objects instead of strings? This way you wouldn't need the dreaded/ugly triple-equal operator. (Sidenote: this is also super confusing for a C# developer since it works much more differently than C#'s 'typeof' keyword)

Problems with is: it needs to be used along with an auxiliar function. This is too verbose. Add a function for every type guard? If you have 3 type guards in a function, you need 4 functions! (Sidenote: this is also super confusing for a C# developer since it works much more differently than C#'s 'is' keyword)

Problems with instanceof: only works with constructors, doesn't work with primitive types. (Sidenote: the mere existence of this keyword makes C# devs confused because the difference of it is not clear from the 'is' keyword, and it doesn't seem to be a better version of is either, because it's only limited to a very specific constructor use case!)

Problems with as keyword: it's not a real dynamic cast, like in C#. See https://decembersoft.com/posts/typescript-vs-csharp-as-keyword/ (I still don't understand the purpose of as in typescript TBH.)

We need a new type guard that can get rid of all the problems of all use cases above.
My initial proposal would be "typedefof" or "isoftype" keywords, for which I put examples below (feel free to propose more keywords or approaches please).

Examples

let x: string = "hello";
if (typedefof x == typedefof string) {
   ...
}
let x: string = "hello";
if (x isoftype string) {
   ...
}

Key point of this feature: simplicity!

Checklist

My suggestion meets these guidelines:

  • [?] This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • [?] This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Activity

  1. changed the title [-]Sane alternative to `typeof` | `is` | `instanceof` ?[/-] [+]Sane alternative to `typeof` | `is` | `as` | `instanceof` ?[/+] on Nov 5, 2018
  2. DanielRosenwasser commented on Nov 5, 2018

    @DanielRosenwasser
    Member

    While I empathize with the many ways to do things in a slightly subpar way, instanceof and typeof are just the tools that ECMAScript gives us. Type predicates (signatures with x is Foo) are a way of communicating something about what a function is checking which is the closest way of overlaying erasable types on existing constructs. as is just a way of convincing the type system that something has some type; like you said, it's not a cast in the C# sense which is why it's called a "type assertion" rather than a "type cast".

    So this doesn't really seem like it's in the scope of the project since we're trying not to add anything with runtime impact that's not specified by ECMA262.

  3. knocte commented on Nov 5, 2018

    @knocte
    Author

    not to add anything with runtime impact

    Is the proposal I gave really a runtime-impact suggestion? I imagine typedefof could transpile to typeof underneath.

  4. knocte commented on Nov 12, 2018

    @knocte
    Author
  5. superamadeus commented on Nov 14, 2018

    @superamadeus

    What you're asking for isn't really isn't possible. There would be many cases where the compiler wouldn't be able to know at compile time whether to use typeof or instanceof under the hood, creating edge cases which are just as confusing as the current way of doing things.
    You can roll your own convenience type guard if you want.

    // null test
    function isOfType(value: unknown, type: null): value is null;
    
    // undefined test
    function isOfType(value: unknown, type: undefined): value is undefined;
    
    // instanceof test
    function isOfType<T extends isOfType.ConstructorFn>(value: unknown, type: T): value is InstanceType<T>;
    
    // typeof test
    function isOfType<T extends isOfType.TypeNames>(value: unknown, type: T): value is isOfType.TypeFromName<T>;
    
    // implementation
    function isOfType(value, type): boolean {
      // typeof test case
      if (typeof type == "string") {
        return typeof value == type;
      }
    
      // null/undefined test case
      if (type == null) {
        return value == null;
      }
    
      // instanceof test case
      return value instanceof type;
    }
    
    // namespace for the type guard
    export namespace isOfType {
      // possible return values of `typeof`
      export type TypeNames =
        "string" |
        "number" |
        "boolean" |
        "function" |
        "undefined" |
        "object";
    
      // type mapping these values to their types
      export type TypeFromName<TTypeName extends TypeNames> =
        TTypeName extends "string" ? string :
        TTypeName extends "number" ? number :
        TTypeName extends "boolean" ? object :
        TTypeName extends "function" ? Function :
        TTypeName extends "undefined" ? undefined :
        TTypeName extends "object" ? object : never;
    
      // a type describing a constructor
      export type ConstructorFn<T = any> = {
        new(...args: any): T;
      }
    }

    Playground link (with usage examples)

  6. spiritwindy commented on Nov 19, 2018

    @spiritwindy

    superamadeus

    let v:any
    if( isOfType(v,"object")){
    	v// toObject
    }
    

    isOfType checks the variable type, throws an exception if it does not match
    Do not use if to determine whether you can infer the type of v
    effect::

    let v:any
    isOfType(v,"object")
    v//is object
    
  7. superamadeus commented on Nov 20, 2018

    @superamadeus

    @spitWind

    I see what you're getting at. The pattern you're describing would work at runtime but there's no way to communicate your intentions (that you're asserting that type type of v must be "object") to the type system.

    I'm trying to think of a situation where this is useful though. I understand using sanity checks to throw if types are invalid, but I can't think of a scenario where it would make sense to use a non-conditional type guard to infer the type of a value rather than typing it appropriately. Example how I would do what you described:

    let v: object;
    
    assertTrue( isOfType(v, "object") ); // throws if false
    
    // v is object
    
  8. knocte commented on Dec 6, 2018

    @knocte
    Author

    Ryan Cavanaugh (@RyanCavanaugh) can someone answer comment #28337 (comment) before closing this at least?

  9. superamadeus commented on Dec 6, 2018

    @superamadeus

    Andres G. Aragoneses (@knocte)

    It wouldn't just transpile to typeof. There would have to be a runtime decision whether or not to use typeof or instanceof based on the values used. This is a runtime impact because you're adding a non-ECMA262 syntax change which changes the output code. This is against TypeScript's design goals.

    Goals
    ...
    8. Avoid adding expression-level syntax.
    ...
    Non-Goals
    ...
    5. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
    6. Provide additional runtime functionality or libraries. Instead, use TypeScript to describe existing libraries.

  10. knocte commented on Dec 7, 2018

    @knocte
    Author

    This is a runtime impact because you're adding a non-ECMA262 syntax change

    I'm not advocating for a change! I'm advocating for a new operator.

  11. DanielRosenwasser commented on Dec 7, 2018

    @DanielRosenwasser
    Member

    I'm not advocating for a change! I'm advocating for a new operator.

    That new operator

    1. Could conflict with new syntax so we tread lightly there, and
    2. has runtime which is definitely, completely, 100% out of scope.

    can someone answer comment #28337 (comment) before closing this at least?

    isOfType checks the variable type, throws an exception if it does not match

    Yes, that is code that has runtime impact.

  12. superamadeus commented on Dec 7, 2018

    @superamadeus

    Andres G. Aragoneses (@knocte) Okay, as far as TypeScript is concerned, a new operator falls under the realm of "expression-level syntax" so that's out of scope.

  13. knocte commented on Dec 8, 2018

    @knocte
    Author

    isOfType checks the variable type, throws an exception if it does not match

    Yes, that is code that has runtime impact.

    I never proposed isOfType operator to throw an exception, that was a comment from @spitWind . I'm advocating for a new type guard that is dynamic (in the same way the keyword as in C# is a dynamic cast, not a static cast).

    a new operator falls under the realm of "expression-level syntax" so that's out of scope.

    Can you confirm this Daniel Rosenwasser (@DanielRosenwasser) ?

  14. YowaiCoder commented on Dec 8, 2018

    @YowaiCoder

    See TypeScript Design Goals.
    Typescript is not C#, or maybe you can fork one, make it less restrained, and start to convince people to use it 😜

  15. kitsonk commented on Dec 10, 2018

    @kitsonk
    Contributor

    Andres G. Aragoneses (@knocte)

    a new operator falls under the realm of "expression-level syntax" so that's out of scope.

    Can you confirm this

    Daniel already confirmed it with his point 2.

    1. has runtime which is definitely, completely, 100% out of scope.

    A runtime emit implies expression-level syntax.

  16. knocte commented on Dec 10, 2018

    @knocte
    Author

    I don't see how a new operator that would transpile into existing javascript operators would have runtime impact.

  17. superamadeus commented on Dec 10, 2018

    @superamadeus

    Andres G. Aragoneses (@knocte) It is an addition to the language to add a feature that is not in the ECMA262 specification, which changes the runtime code output of the transpiler. TypeScript tends to avoid adding expression-level syntax features which affect the runtime code output of transpilation unless it is a feature which is explicitly described in the ECMA262 specification.

    It doesn't matter if it transpiles into existing JavaScript operators. If it's not an operator in JavaScript (ES2018), it's out of scope as a feature in TypeScript.

    It is against TypeScript's goals to add features like new operators to the language unless they're defined in ECMA262

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Out of ScopeThis idea sits outside of the TypeScript language design constraintsSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions