Skip to content

Operator to ensure an expression is contextually typed by, and satisfies, some type #7481

Closed
@magnushiie

Description

@magnushiie
Contributor

Sometimes it's necessary (e.g. for guiding type inference, for ensuring sub-expression conforms to an interface, or for clarity) to change the static type of an expression. Currently TypeScript has the as (aka <>) operator for that, but it's dangerous, as it also allows down-casting. It would be nice if there was another operator for implicit conversions only (type compatibility). I think this operator should be recommended in most cases instead of as.

This operator can be implemented as a generic function, but as it shouldn't have any run-time effect, it would be better if it was incorporated into the language.

function asType<T>(value: T) {
  return value;
};

EDIT: Due to parameter bivariance, this function is not equivalent to the proposed operator, because asType allows downcasts too.

Activity

RyanCavanaugh

RyanCavanaugh commented on Mar 11, 2016

@RyanCavanaugh
Member

Can you post a few examples of how you'd like this to work so we can understand the use cases?

magnushiie

magnushiie commented on Mar 12, 2016

@magnushiie
ContributorAuthor

One example very close to the real-world need (I'm trying to get react and react-redux typings to correctly represent the required/provided Props):

import { Component } from "react";
import { connect } from "react-redux";

// the proposed operator, implemented as a generic function
function asType<T>(value: T) {
  return value;
};

// in real life, imported from another (actions) module
function selectSomething(id: string): Promise<void> {
  // ...
  return null;
}

interface MyComponentActions {
  selectSomething(id: string): void;
}

class MyComponent extends Component<MyComponentActions, void> {
  render() {
    return null;
  }
}

// I've changed the connect() typing from DefinitelyTyped to the following:
// export function connect<P, A>(mapStateToProps?: MapStateToProps,
//                            mapDispatchToProps?: MapDispatchToPropsFunction|A,
//                            mergeProps?: MergeProps,
//                            options?: Options): ComponentConstructDecorator<P & A>;

// fails with "Argument of type 'typeof MyComponent' not assignable" because of 
// void/Promise<void> mismatch - type inference needs help to upcast the expression
// to the right interface so it matches MyComponent
export const ConnectedPlain = connect(undefined, {
  selectSomething,
})(MyComponent);

// erronously accepted, the intention was to provide all required actions
export const ConnectedAs = connect(undefined, {
} as MyComponentActions)(MyComponent);

// verbose, namespace pollution
const actions: MyComponentActions = {
  selectSomething,
};
export const ConnectedVariable = connect(undefined, actions)(MyComponent);

// using asType<T>(), a bit verbose, runtime overhead, but otherwise correctly verifies the
// expression is compatible with the type
export const ConnectedAsType = connect(undefined, asType<MyComponentActions>({
  selectSomething,
}))(MyComponent);

// using the proposed operator, equivalent to asType, does not compile yet
export const ConnectedOperator = connect(undefined, {
  selectSomething,
} is MyComponentActions)(MyComponent);

I've called the proposed operator in the last snippet is.

The other kind of scenario is complex expressions where it's not immediately obvious what the type of the expression is and helps the reader understand the code, and the writer to get better error messages by validating the subexpression types individually. This is especially useful in cases of functional arrow function expressions.

A somewhat contrived example (using the tentative is operator again), where it's not immediately obvious what the result of getWork is, especially when it's a generic function where the result type depends on the argument type:

const incompleteTasks = (tasks: Task[]) => tasks.filter(task => !(getWork(task.currentAssignment) is Todo).isComplete);
added
SuggestionAn idea for TypeScript
Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
on Mar 12, 2016
DanielRosenwasser

DanielRosenwasser commented on Mar 12, 2016

@DanielRosenwasser
Member

I ran into something similar when I was patching up code in DefinitelyTyped - to get around checking for excess object literal assignment, you have to assert its type, but that can be a little extreme in some circumstances, and hides potential issues you might run into during a refactoring.

There are also scenarios where I want to "bless" an expression with a contextual type, but I don't want a full blown type assertion for the reasons listed above. For instance, if a library defines a type alias for its callback type, I want to contextually type my callback, but I _don't_ want to use a type assertion.

In other words, a type assertion is for saying "I know what I'm going to do, leave me a alone." This is more for "I'm pretty sure this should be okay, but please back me up on this TypeScript".

RyanCavanaugh

RyanCavanaugh commented on Mar 14, 2016

@RyanCavanaugh
Member

Sounds a lot like #2876?

magnushiie

magnushiie commented on Mar 14, 2016

@magnushiie
ContributorAuthor

If I understand #2876 correctly, it's still a downcast (i.e. bypassing type safety). What I was proposing here is an upcast (i.e. guaranteed to succeed at runtime or results in compile time error). Also, while <?> seems a bit like magic, the is operator is as straightforward as assigning to a variable with a defined type or passing an argument to a function with a parameter that has a defined type.

I think the best example of this operator exists in the Coq language:

Definition id {T} (x: T) := x. 
Definition id_nat x := id (x : nat).
Check id_nat.
id_nat
     : nat -> nat

Here, the expression x : nat is a type cast, where Coq's type cast is not dynamic but static (and mostly used in generic scenarios, like the ones I mentioned above) - here it means id_nat's argument type is restricted to be a nat.

chilversc

chilversc commented on Jun 9, 2016

@chilversc

Another case for this is when returning an object literal from a function that has a type union for it's return type such as Promise.then.

interface FeatureCollection {
  type: 'FeatureCollection'
  features: any[];
}

fetch(data)
  .then(response => response.json())
  .then(results => ({ type: 'FeatureCollection', features: results }));

This gets quite tricky for intellisense in VS because the return type from then is PromiseLike<T> | T. Casting allows intellisense to work, but as mentioned it can hide errors due to missing members.

Also the error messages when the return value is invalid are quite obtuse because they refer to the union type. Knowing the intended type would allow the compiler to produce a more specific error.

magnushiie

magnushiie commented on Sep 14, 2016

@magnushiie
ContributorAuthor

@chilversc I'm not sure how an upcast can help with your example. Could you show how it would be used, using the above asType function (which is the equivalent to the operator I'm proposing). Note that due to parameter bivariance, the current compiler would not always give an error on invalid cast.

chilversc

chilversc commented on Sep 18, 2016

@chilversc

Odd, I thought I had a case where an assignment such as let x: Foo = {...}; would show a compile error while a cast such as let x = <Foo> {...}; would not.

The cast was required to get the object literal to behave correctly as in this case:

interface Foo {
    type: 'Foo',
    id: number;
}
let foo: Foo = { type: 'Foo', id: 5 };
let ids = [1, 2, 3];

//Error TS2322 Type '{ type: string; id: number; }[]' is not assignable to type 'Foo[]'.
//Type '{ type: string; id: number; }' is not assignable to type 'Foo'.
//Types of property 'type' are incompatible.
//Type 'string' is not assignable to type '"Foo"'.
let foosWithError: Foo[] = ids.map(id => ({ type: 'Foo', id: id }));

let foosNoErrorCast: Foo[] = ids.map(id => ({ type: 'Foo', id: id } as Foo));
let foosNoErrorAssignment: Foo[] = ids.map(id => {
    let f: Foo = {type: 'Foo', id: id};
    return f;
});
normalser

normalser commented on Sep 28, 2016

@normalser

Could we just use is the same way as as ?

interface A {
   a: string
}

let b = {a: 'test'} as A // type: A, OK
let c = {a: 'test', b:'test'} as A // type: A, OK
let d = {a: 'test'} is A // type: A, OK
let e = {a: 'test', b:'test'} is A // error, b does not exist in A
aluanhaddad

aluanhaddad commented on Dec 29, 2016

@aluanhaddad
Contributor

@wallverb that is really clever and really intuitive. Interestingly, it also provides a manifest way of describing the difference between the assignability between fresh object literals target typed by an argument vs existing objects that conform to the type of that argument.

164 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fix AvailableA PR has been opened for this issueFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Participants

      @jtlapp@shicks@chilversc@cefn@ethanresnick

      Issue actions

        Operator to ensure an expression is contextually typed by, and satisfies, some type · Issue #7481 · microsoft/TypeScript