Typescript: Sane alternative to `typeof` | `is` | `as` | `instanceof` ?

Created on 5 Nov 2018  ·  16Comments  ·  Source: microsoft/TypeScript

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
  • [x] 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
  • [x] This isn't a runtime feature (e.g. new expression-level syntax)
Out of Scope Suggestion

Most helpful comment

@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

All 16 comments

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.

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.

@DanielRosenwasser ping?^

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)

@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

@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

@RyanCavanaugh can someone answer comment https://github.com/Microsoft/TypeScript/issues/28337#issuecomment-435777869 before closing this at least?

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

  1. Avoid adding expression-level syntax.
    ...
    Non-Goals
    ...
  2. 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.
  3. Provide additional runtime functionality or libraries. Instead, use TypeScript to describe existing libraries.

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.

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 https://github.com/Microsoft/TypeScript/issues/28337#issuecomment-435777869 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.

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

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 @DanielRosenwasser ?

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 :stuck_out_tongue_winking_eye:

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

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

@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

Was this page helpful?
0 / 5 - 0 ratings