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

Automatic type guard #51409

Closed
3 of 5 tasks
rentalhost opened this issue Nov 4, 2022 · 5 comments
Closed
3 of 5 tasks

Automatic type guard #51409

rentalhost opened this issue Nov 4, 2022 · 5 comments

Comments

@rentalhost
Copy link

rentalhost commented Nov 4, 2022

Suggestion

πŸ” Search Terms

automatic type guard, smart type guard

βœ… Viability 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. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Typescript can perform type narrowing in certain circumstances intelligently (and every day I am more surprised). However, types are not JS compatible, so something like instanceof SomeType is not allowed, as during conversion the type is lost.

In this case, we can perform a type guard manually, so that we can determine that the information we are receiving is compatible with the expected type.

For example (Playground):

type Animal = { name: string; age: number };
type Vehicle = { name: string; brand: string };

function isAnimal(x: any): x is Animal {
  return "age" in x;
}
function isVehicle(x: any): x is Vehicle {
  return "model" in x;
}

function log(item: Animal | Vehicle) {
  if ('age' in item) console.log(item.age);
  else if ('brand' in item) console.log(item.brand);
}

log({ name: "Rex", age: 3 });
log({ name: "Corolla", brand: "Toyota" });

This will work perfectly. But suppose at some point I say that Vehicle will now have age and Animal will have breed.

type Animal = { name: string; age: number, breed: string };
type Vehicle = { name: string; age: number; brand: string };

In this case, our isAnimal() type guard will incorrectly interpret that a vehicle is an animal. Because the type guard works on top of information that is now common to both.

To solve the problem, we can change the way isAnimal() works, making it work by checking if it has breed instead:

function isAnimal(x: any): x is Animal {
  return "breed" in x;
}

However, whenever something similar happens, we need to be careful to make sure that our type guard conforms to the uniqueness of the type, while not conflicting with another possible similar type (not shown here).

So my suggestion is that Typescript could actually use item instanceof <Type>, and it could intelligently infer (whenever possible) a type guard for the expected type.

function log(item: Animal | Vehicle) {
  if (item instanceof Animal) console.log(item.age);
  else if (item instanceof Vehicle) console.log(item.brand);
}

In this case, even though Animal is a type (which in principle is impossible nowadays), Typescript would automatically check how it could safely type narrow inside this function, based on the type of data of Animal in relation to the union Animal | Vehicle.

So in our first example, the code would convert to something like:

function log(item) {
    if ('age' in item) console.log(item.age);
    else if ('brand' in item) console.log(item.brand);
}

And if the signature of Animal and Vehicle changes, as in the second example, the generated code would automatically change to ensure that the function has the same behavior:

function log(item) {
    if ('breed' in item) console.log(item.age);
    else if ('age' in item) console.log(item.brand);
}

Note: in this else if() we can use 'brand' in item or 'age' in item because although age belongs to both types, if() has already captured the Animal type.

@fatcerberus
Copy link

This violates the design goals of TS, in particular these two checkboxes in the issue template:

This could be implemented without emitting different JS based on the types of the expressions

This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)

That TypeScript types are 100% invisible at runtime is by design.

@rentalhost
Copy link
Author

@fatcerberus Thanks for reply!

I understand. But is there any reason why types cannot be "type guarded automagically" that makes this rule inviolable? I mean, I understand it's good to have rules, and I really imagine there's a reason, but this exception could make Typescript smarter by allowing instanceof on types.

And, in this case, there would really be a need to have a JS code elaboration during the process by the TS. But how much is this a problem that cannot be treated? Are there practical reasons for this, for example?

For example, I would understand that complex cases TS could not allow the use of instanceof Type (which should probably show an error indicating that TS cannot infer automatically), but there are simpler cases, like the example itself, that this could be done without the user having knowledge about type guard or needing to worry about it.

@rentalhost
Copy link
Author

@MartinJohns Yes, but that's exactly what I'd like to suggest.

As instanceof is currently used exclusively for classes on JS, so instanceof Type would be currently invalid code, which would avoid any BC.

Another option would be to make this clearer, for example allowing the use of typeof in this type of context (eg. if(item typeof Type)), but I believe it could generate BC and "cognitive conflict" with JS typeof.

Or creating a new operator, for example istype or is only (eg if(item is Type)).

Or as a method of type itself: if (Animal.accept(item)).

@MartinJohns
Copy link
Contributor

It's against TypeScripts Language Design Goals and was rejected several times already.

@fatcerberus
Copy link

fatcerberus commented Nov 4, 2022

@rentalhost #47658 (comment)

Per @RyanCavanaugh, the TS dev lead:

Regarding design goals - we're extremely committed to the erasability of the type system. Keeping the type system erasable is a very strong invariant that allows us to do other important things. One of the reasons we're able to make the type system more powerful and expressive from version to version is that we have the flexibility to e.g. change type inference at a particular position from { x: string } & { y: number } to { x: string; y: number } without worrying about breaking the runtime behavior of anyone's programs.

This isn't a theoretical concern: people already @ us about when we change declaration emit in ways that produce representationally-distinguishable but semantically-identical types. Taking this to the next level wherein one person's bug about a conditional type not immediately resolving becomes a breaking change in someone else's program, at runtime, in ways that are going to be much more subtle than type error vs not-a-type-error -- it's not a Pandora's Box we're keen to open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants