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

typeof expressions should return an equivalent of a guard #37868

Open
4 of 5 tasks
Barbiero opened this issue Apr 9, 2020 · 1 comment
Open
4 of 5 tasks

typeof expressions should return an equivalent of a guard #37868

Barbiero opened this issue Apr 9, 2020 · 1 comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@Barbiero
Copy link

Barbiero commented Apr 9, 2020

Search Terms

typeof guard array filter

Suggestion

Currently, typeof expressions return a boolean as expected. However, they are also special in that they narrow the types of their operands. In a similar vein, we can use type guards as the return value of functions to narrow(or assert) the types of their parameters:

declare const broad: string | number | object;
if(typeof broad === 'number') {
    // broad is now a number, not a string nor an object
}

declare function isNumber(param: any): param is number;

if(isNumber(broad) {
  // broad is now a number, not a string nor an object
}

This is all fine, but suppose you write a very specific function:

// Current return type: boolean
// Wish it was `param is number`
const isNumber = (param: any) => typeof param === 'number'

if (isNumber(broad)) {
   // param is possibly a string or object?!
}

This is IMO a special case that should be handled; particularly, when a typeof expression(or any expression that narrows a parameter's type) is the return value of a function, TS should infer that the function is a guard.

Notice that, unlike the discussion in #6015, this issue is about the inferred type of a function which has a return value of a narrowing-expression. We could go further:

const isObject = (o: unknown) => typeof o === 'object';
// type for isObject == (o:unknown) => o is object

const isElement = (o: object) => o instanceof Element;
// type for isElement == (o:object) => o is Element

const unknownIsElement = (o: unknown) => isObject(o) && isElement(o);
// type for unknownIsElement == (o:unknown) => o is Element

const numberIsElement = (o:unknown) => isNumber(o) && isElement(o); // error: o is number so isElement can't be called

(by the way, I searched but couldn't find a similar one. If this is a duplicate I apologize!)

Use Cases

The most immediate use case is for array filter:

// error: (string|object)[] not assignable to string[]
const stringElements: string[] = elements.filter(e => typeof e === 'string');

// Works, but repetitive
const stringElements: string[] = elements.filter((e): e is string => typeof e === 'string');

I'm sure this type of change would make guards far more powerful as well, since we could create functions combine JS-checks(typeof, instanceof) or even TS-checks(like discriminant variable checking) without having to explicitly write the guard expression(which, IMO is as good as a type assertion at the moment).

Examples

(see above)

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

This is possibly a breaking change, but IMO a good one as it would highlight actual programming errors for code that breaks

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Apr 17, 2020
@sploders101
Copy link

I also think this would be an extremely useful convention, but would like to take it a step further and say that type guards should be extended to be able to create typeof-like guards, where one function can definitively state the type. For example:

function(obj: any): obj is TypeGuardAdv<{
    type1: Type1,
    type2: Type2,
    type3: Type3,
}> {
    if(obj.prop1) return "type1";
    if(obj.prop2) return "type2";
    if(obj.prop3) return "type3";
}

I just used a generic TypeGuardAdv to demonstrate the concept, as I'm not entirely sure what the resulting syntax would be.

I'm not sure if this next suggestion is in scope for this issue or not, but it would also be nice to be able to store these type annotations to variables. Consider the following example:

interface BaseType {
    name: string;
}
interface MyType extends BaseType {
    myProp: string;
}
declare const isMyType: (obj: any) => obj is MyType;

const obj = { name: string, myProp: string };
const objIsMyType = isMyType(obj);

if(objIsMyType) {
    doSomethingExtraWith(obj.myProp);
}
doSomethingWith(obj);
if(objIsMyType) {
    extraFinalization(obj.myProp);
}

If a type guard function requires extensive checks, then it would be inefficient to run them more than once if obj is constant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants