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

User-defined type assertions #17760

Closed
maghis opened this issue Aug 12, 2017 · 18 comments
Closed

User-defined type assertions #17760

maghis opened this issue Aug 12, 2017 · 18 comments
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@maghis
Copy link

maghis commented Aug 12, 2017

Proposal: support user-defined type assertions (keyword as) in addition to user-defined type guards (keyword is).

A user-defined type assertion is a function with a return type expressed as in the following example (continuing the handbook example in "Advanced Types").

function assertIsFish(pet: Fish | Bird): pet as Fish {
    if (!isFish(pet))
        throw new Error("birds are not supported");
}

assertIsFish(pet);

pet.swim();

:pet as fish means that, if the function assertIsFish returns without throwing, pet is automatically asserted to be of type Fish.

Use-cases:

  1. Validators: user-defined type assertions make it possible to encapsulate in functions the type narrowing that is currently available through control flow analysis. They also allow types that better describe the behavior of many existing libraries, including: node asserts, data validators that throw detailed errors, many test frameworks.

  2. Polyfills: enables describing functions that modify objects by adding new properties.

addName<T>(obj: T): obj as T & { name: string } {
    obj.name = "Mike";
}

const obj = {};
addName(obj);

obj.name.indexOf("i");

Validator examples:

simple undefined checking

function notUndefined<T>(obj: T | undefined): obj as T {
    if (obj === undefined)
        throw new Error("invalid object");
}

const myObj: string | undefined = {} as any;

// [ts] Object is possibly 'undefined'.
myObj.indexOf("");

notUndefined(myObj);

// no errors
myObj.indexOf("");

joi-like validation currently requires implementing the schema description of an object for runtime checking and copying the schema to an interface to have static type checking while the static type could be inferred by the implementation of the schema

interface Schema<T> {
    validate(obj: any): obj is T;
}

function attemptValidation<T>(obj: any, schema: Schema<T>): obj as T {
    if (!schema.validate(obj))
        throw new Error("very detailed object validation error");
}

// complex schema implements T and T is inferred by the schema construction
// this is already possible
const complexSchema = Joi.object({
    myField: Joi.date().iso()
});

attemptValidation(objFromTheRealWorld, complexSchema);

const day = objFromTheReadWorld.myField.getDay();
@jcalz
Copy link
Contributor

jcalz commented Aug 14, 2017

I like the idea. I've needed something like this before, and the way I've done it is by immediately throwing if the relevant type guard returns false. So you can have the somewhat cumbersome:

function unsafeNarrow<T>(x: any): x is T {
  return true;
}
const obj = {height: 42};  // obj is {height: number}
if (!unsafeNarrow<{ name: string }>(obj)) throw null;
// obj is {height: number} & {name: string} from here on
obj.name = 'mike';  // no error

It would be nice to have something less ugly. Not sure if the x as T syntax is the way to go? Maybe we can continue to use x is T but the control flow analysis can tell that the returned value is always true and therefore any call to the type guard does automatic narrowing? So something like true & (x is T), if it compiled, would be a possibility.

@maghis
Copy link
Author

maghis commented Aug 14, 2017

@jcalz I agree, I'm open to any syntax, that was just a suggestion as a starting point.

Another really annoying thing is in tests, where you first assert that an object is truthy and then you have to explicitly check for it with an if or a type assertion before you can assert on values of the object.

// obj comes from some test invocation
assert(obj);
// we already know that obj is not undefined but
if (obj) {
    assert.equal(obj.field, "foo");
}

@mhegazy mhegazy added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Aug 21, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Aug 22, 2017

also related to #10421

@TazmanianD
Copy link

I was just looking for something like this as well. I agree with @jcalz that it seems like the existing syntax should continue to work and it's just the compiler code analysis can take into account that the failure in the function means the code throws and exits and a non-failure confirms the assertion.

@maghis
Copy link
Author

maghis commented Oct 19, 2017

@TazmanianD I agree, we don't necessarily need new syntax, it was just nice to know by just looking at the function signature that I don't have to check for a return value.

@mitchellwills
Copy link

I was just looking for something like this as well. I don't think you can use the same x is T return value syntax because you won't always be able to do control flow analysis on the called function, such as if it's referencing a function declaration.

I'm also not sure that the original return value x as T syntax works either since it doesn't really have any thing to do with the return value of the function, just that the function actually returned. It's more of a postcondition. Having a syntax that goes in the return value slot would prohibit having an actual return value from the function. For example:

// where do I write an explicit return type?
function initializeAndGetId<T>(obj: T): obj as T&{id: number} { 
  (obj as any).id = generateId();
  return obj.id;
}

const value = {};
const id = initializeAndGetId(value);
// this should compile since value should now be assumed to have the type {id: number}
assert(id === value.id);

Unfortunately, I don't really have any better suggestions.

@MicahZoltu
Copy link
Contributor

There are a number of ES built-in functions that would greatly benefit from this like Number.isFinite(). Currently the following will not work:

function method(param: any) {
    const myNumber: number = Number.isFinite(param) ? param : 5
}

Instead, you must do something like:

function method(param: any) {
    const myNumber: number = (typeof param === 'number' && Number.isFinite(param)) ? param : 5
}

@maghis
Copy link
Author

maghis commented Jun 5, 2018

@MicahZoltu In that case it's because param is already defined as number in the type definitions.
You could achieve the same results with the existing syntax if the function was defined as (param: any) => param is number.

@MicahZoltu
Copy link
Contributor

Adding param is number was my hope, but apparently it doesn't actually work in this case as described here: #24436 (comment)

@maghis
Copy link
Author

maghis commented Jun 6, 2018

@MicahZoltu that makes sense, I didn't think about the side effects.

@aciccarello
Copy link

I'm also running into a similar situation to @maghis in tests as #17760 (comment). I'd like for assert.isDefined(param) to be able to be typed the same as an if that throws an error.

let callbackParam: string | undefined;
doesThisCallTheCallback((p) => callbackParam = p);

if (callbackParam == null) { // Want this to be in assert.isDefined()
  throw new Error("TypeScript knows callbackParam is string if this hasn't thrown yet");
}
assert.isDefined(callbackParam); // throws if undefined
assert.equals(callbackParam.split(' ').length, 5); // Should know callbackParam is a string here

@RyanCavanaugh
Copy link
Member

@aciccarello FYI that behavior is tracked at #10421 / #8655

@ikokostya
Copy link
Contributor

ikokostya commented Jan 29, 2019

@RyanCavanaugh Issue #8655 was closed without good explanation. Many questions in the thread are still without answers. Suggested throw expressions doesn't solve all cases, e.g. process.exit() in Node.js.

@jwalton
Copy link

jwalton commented Mar 29, 2019

Testing with chai is another obvious place where this would be nice:

import { expect } from 'chai';

it('should do the thing', function() {
  const result = doTheThing();
  expect(result).to.exist; 
  expect(result!.foo).to.equal(7); // I don't want to type this !.  :P
});

I feel like there should be some sort of "condtional type" syntax that could be used here. Something like:

function assertNotUndefined(x: string | undefined) : x is undefined ? true : never {
    if(x === undefined) {
        throw new Error('undefined');
    }
    return true;
}

@mheiber
Copy link
Contributor

mheiber commented Jun 4, 2019

In order to meet our use case, user-defined type assertions would have to be intersectable with normal return values. In the below, I use this syntax instead of as T: Ensures<T>.

For example, suppose that you have a UI framework where children only have .layout property if they have been added to a parent.

It would be really helpful if we could plug in to the same sort of flow analysis used by type guards.

Example of desired behavior using Ensures:

// adding a button to a container gives it a 'layout' property
const button = new Button();
button.layout; // error because we don't know if it is added to a container yet
if (shouldAdd) {
    container.addChild(button);
    button.layout; // ok because of the `Ensures` type (defined below)
}
else {
    button.layout; // error because control flow didn't hit anything that ensures `button.layout` defined
}

// definitions
class Component { /* ... */ }
class Container extends Component {
    children: Component[] = [];
    addChild<T extends Component>(child: T): this & Ensures<T is T & { layout: Layout }> {
        this.children.push(child)
    }
}

@ethanresnick
Copy link
Contributor

ethanresnick commented Feb 24, 2020

This should probably be closed now that #32695 has been merged (although that PR still doesn't cover the request in the comment immediately above, to be able to return a normal value and assert with the same function).

@maghis
Copy link
Author

maghis commented Mar 27, 2020

I think so, asserts covers pretty much all the listed use cases.
Anyone against me closing?

@DanielRosenwasser
Copy link
Member

Looks like this was fixed in #32695

@DanielRosenwasser DanielRosenwasser added this to the TypeScript 3.7.0 milestone Apr 28, 2022
@DanielRosenwasser DanielRosenwasser added Fixed A PR has been merged for this issue and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Apr 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests