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

Permit functions that return a value to also serve as a type guard #31376

Open
5 tasks done
sethfowler opened this issue May 13, 2019 · 2 comments
Open
5 tasks done

Permit functions that return a value to also serve as a type guard #31376

sethfowler opened this issue May 13, 2019 · 2 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@sethfowler
Copy link

Search Terms

Linear type, affine type, type guard

Suggestion

It would be very helpful to allow a function to serve as a type guard, but also return an unrelated value.

Use Cases

This can be used to express type changes as a result of mutating operations, covering some of the use cases of e.g. Rust's affine types. (See also #16148.)

Examples

Consider this example, compiled with --strictNullChecks:

type NonEmptyArray<T> = {
  pop(): T;
} & Array<T>;

function isNonEmpty<T>(array: Array<T>): array is NonEmptyArray<T>;
function isNonEmpty(array: Array<unknown>): boolean {
  return array.length > 0;
}

let array: string[] = ['element'];
if (isNonEmpty(array)) {  // Guard gives 'array' type NonEmptyArray<string>.
  const elem1: string = array.pop();  // Works. This is correct.
  const elem2: string = array.pop();  // Also works, but elem2 will be undefined at runtime!
}

We could make this correct if pop() could both return a value and behave as a type guard. This isn't great syntax, but nonetheless consider if this was supported:

type NonEmptyArray<T> = {
  pop(): T && this is Array<T>;
} & Array<T>;

function isNonEmpty<T>(array: Array<T>): array is NonEmptyArray<T>;
function isNonEmpty(array: Array<unknown>): boolean {
  return array.length > 0;
}

let array: string[] = ['element'];
if (isNonEmpty(array)) {  // Guard gives 'array' type NonEmptyArray<string>.
  const elem1: string = array.pop();  // Returns a string and gives 'array' type Array<string>.
  const elem2: string = array.pop();  // Doesn't compile; pop() returns 'string | undefined'!
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code

This can use a new, previously invalid syntax to avoid affecting any existing program.

  • 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, etc.)

There's no change in the code that's emitted; this feature would exist purely at the level of the type system.

I believe that it would. It seems to be aligned well, in particular, with "Statically identify constructs that are likely to be errors."

@sethfowler sethfowler changed the title Permit functions that return a value to serve as a type guard Permit functions that return a value to also serve as a type guard May 13, 2019
@sethfowler sethfowler changed the title Permit functions that return a value to also serve as a type guard Permit functions that return a value to also serve as a type assertion May 13, 2019
@sethfowler sethfowler changed the title Permit functions that return a value to also serve as a type assertion Permit functions that return a value to also serve as a type guard May 13, 2019
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels May 13, 2019
@AlCalzone
Copy link
Contributor

Could this be a solution to #18781 ?

@sethfowler
Copy link
Author

That one could be solved today, I think; seems like the blocker there is getting consensus that it's a good idea.

The related #13086 could definitely be solved by this, though. In that one, the issue is this:

const map = new Map<string, string>();
if (map.has('key')) {
  const a: string = map.get('key');  // Error: Map<string, string>#get() returns 'string | undefined'.
}

The ideal thing would be to narrow the type of map such that map.get('key') would return string instead of string | undefined. As folks pointed out in #13086, though, the problem is that you could call map.delete('key') between the call to map.has('key') and map.get('key'). The idea in this issue, or some variant of it, could solve that by widening the type of map again when you call map.delete('key'). (Or by narrowing it such that map.get('key') returns the type undefined.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants