microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.08k stars 12.5k forks source link

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

Closed magnushiie closed 2 years ago

magnushiie commented 8 years ago

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.

RyanCavanaugh commented 8 years ago

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

magnushiie commented 8 years ago

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);
DanielRosenwasser commented 8 years ago

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 commented 8 years ago

Sounds a lot like #2876?

magnushiie commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 7 years ago

@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.

dead-claudia commented 7 years ago

I like this idea of effectively a static type assertion.

magnushiie commented 7 years ago

@normalser your example about let e = {a: 'test', b:'test'} is A is to me still an up-cast and should succeed as proposed by this issue. I think your expectation is more towards #12936. Although if A is an exact type (as proposed in #12936), the is operator would error as well.

magnushiie commented 7 years ago

Also linking #13788 about the current behavior of the type assertion operator as being both upcast and downcast.

jmagaram commented 6 years ago

I'm new to TypeScript but have already had a need for something like this. I've been Googling for a while now and looked in the TypeScript docs and can't find what I need. I'm really surprised since I assume this capability MUST exist. I think I ran into the problem trying to use Redux and wanted to ensure that I was passing a specific type of object to the connect function. Once I specify the type hint - "this is supposed to be a X" - I'd like the editor to do intellisense and show me what properties need to be filled in, so maybe the type name needs to come first like a safe cast expression.

function doSomething(obj: any) { }

interface IPoint {
    x: number;
    y: number;
}

// This does what I expect. I'm making sure to pass a correct IPoint 
// to the function.
let point: IPoint = { x: 1, y: 2 };
doSomething(point);

// But is there a more concise way to do the above, without introducing a
// temporary variable? Both of these compile but it isn't safe since my IPoint
// is missing the y parameter.
doSomething(<IPoint>{ x: 1 });
doSomething({ x: 1 } as IPoint);

// How about this type of syntax?
doSomething(<IPoint!>{ x: 1 });
doSomething(<Exact<IPoint>>{ x: 1 });
doSomething((IPoint = { x: 1 }));
doSomething({ x: 1 } as IPoint!); // for JSX
doSomething({ x: 1 } is IPoint);
doSomething({ x: 1 } implements IPoint);
RyanCavanaugh commented 6 years ago

@DanielRosenwasser I feel like there must be some way at this point to use mapped types or something to write something that breaks the comparability relationship in a way that it only allows subtypes through?

jmagaram commented 6 years ago

Actually this would work for me. I just need to create a custom function that I use wherever I need an exact type. Seems kind of obvious in retrospect. I'm still surprised this kind of thing isn't built-in.

function exact<T>(item:T): T {
    return item;
}

doSomething(exact<IPoint>({x:1, y:2}));
ackvf commented 6 years ago

I am also looking for a way to specify the type of an inline object I am working with, so that when I ctrl+space I get hints and warnings.

image

By using as I get hints, but it is dangerous as I get no warnings for missing or unknown keys.


There already are type guards that use the parameterName is Type

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

It would be nice to also have this:

getFirebaseRef().push({} is MyItem)

-> now I would get hints for object properties -> now I would get errors for missing and unknown properties

wkrueger commented 6 years ago

image

Does this belong to this issue? (Allow a way to point the type checker to strictly infer strings and numbers as constants)

RyanCavanaugh commented 6 years ago

Update from suggestion review - there are a bunch of scenarios where we want a syntax that looks something like

expr SOMETHING T

Which causes expr to be contextually typed by T, but the type of the entire expression is still expr's inferred type rather than T. An error occurs if expr is not assignable to T (assignability in the other direction is not sufficient).

The problem is what SOMETHING is. We punted a bunch of syntax ideas around but hated all of them. extends sounds too much like it does a runtime thing; typeof is taken; really all new expression syntax is super suspect, etc.. So we're basically blocked on finding some palatable syntactic space for this. Open to ideas but we're not super hopeful yet.

magnushiie commented 6 years ago

Which causes expr to be contextually typed by T, but the type of the entire expression is still expr's inferred type rather than T.

@RyanCavanaugh while I can see how this operator could be useful in some scenarios, your proposed operator is different from what I originally proposed. My proposed operator's type would be T, not typeof expr, and is useful for guiding bottom-up type inference and hiding too specific types, whereas the operator you described is mostly a documentation/debugging tool as it doesn't have any effect to the surrounding code.

dead-claudia commented 6 years ago

This precisely is the minimum functionality I would like to see - we're not concerned about contextual typing, but working around either failed type inference or overload selection. And the easiest way to do this is to have a safe "upcast" that's checked to not also downcast.

On Wed, Aug 22, 2018 at 22:51 Magnus Hiie notifications@github.com wrote:

Which causes expr to be contextually typed by T, but the type of the entire expression is still expr's inferred type rather than T. @RyanCavanaugh https://github.com/RyanCavanaugh while I can see how this operator could be useful in some scenarios, your proposed operator is different from what I originally proposed. My proposed operator's type would be T, not typeof expr, and is useful for guiding bottom-up type inference and hiding too specific types, whereas the operator you described is mostly a documentation/debugging tool as it doesn't have any effect to the surrounding code.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/7481#issuecomment-415299196, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBC_ro8xw4w-ZSGwTZsNBA0s-0CKYks5uTkKOgaJpZM4HurCq .

ackvf commented 6 years ago

I also see a difference in what you propose @RyanCavanaugh. In its simplest form, what I believe is desired from this proposal is that (in my example) the abstract method push of the firebase handle gets the type assurance similar to the push of the typed array.

image

Ideally an is keyword, as was already suggested, is what seems to be the best fit. be would be also fine.

getFirebaseRef().push({} is MyItem)
getFirebaseRef().push({} be MyItem)

In this example, it is like using a type guard

const incompleteTasks = (tasks: Task[]) => tasks.filter(task => !(getWork(task.currentAssignment) is Todo).isComplete);
const incompleteTasks = (tasks: Task[]) => tasks.filter(task => !(getWork(task.currentAssignment) be Todo).isComplete);

// verbose, but eventually does the same thing

function isTodo(task: Todo | SomethingElse): task is Todo {
  return (<Todo>task).isComplete !== undefined;
}

const incompleteTasks = (tasks: Task[]) => tasks.filter(task => {
  const work = getWork(task.currentAssignment)
  if (isTodo(work)) {
    return !work.isComplete
  } else {
    throw new Error('Wrong Type!')
    // actually we want a compile error, not runtime
  }
});
mpawelski commented 6 years ago

@RyanCavanaugh I'm also wondering why you want expr SOMETHING T to be of expr inferred type instead of just T.

This still will be hugely useful feature, used quite often in many scenarios. But I don't see scenarios when I don't want the type to be T.

Like in here: image

I want this type to be SomeInterface not { a: string; b: string: c: string; } because when I see this name in editor hints and error message I know what it is immedietely.

As for SOMETHING. I don't know. I kinda like is proposed earlier. Whatever you'll choose will be fine for me, just don't make it too long, because it will be used often 😜.

Or I'm missing the purpose of this proposal completely? Your proposal mean that expr SOMETHING T will throw compiler error is all cases when var t : T = expr throws also? right?

RyanCavanaugh commented 6 years ago

Scenarios where the expression type is more valuable -

// Today
const a: Partial<Point> = { x: 10 };
// Error, 'x' might be undefined
console.log(a.x.toFixed());
// OK, really shouldn't be
console.log(a.y!.toFixed());

// Desired
const a = { x: 10 } SOMETHING Partial<Point>;
// OK
console.log(a.x.toFixed());
// Error
console.log(a.y!.toFixed());

// Another example (Desired only):
type Neat = { [key: string]: boolean };
declare function fn(x: { m: boolean }): void;
const x = {
    m: true
} SOMETHING Neat;

// Today: Should be OK, isn't
fn(x);
// Today: Should be error, isn't
console.log(x.z);
RyanCavanaugh commented 6 years ago

@mpawelski I think you're off-course here; your example involves a downcast (which is what everyone agrees needs to not be possible) and the display of the type there doesn't depend on any assertions

mpawelski commented 6 years ago

yep, I meant that as in downcast I want new operator to be of type T, maybe I didn't made it clear enough.

Anyway, your examples totally convinced me why it shouldn't. thanks!

dead-claudia commented 6 years ago

@RyanCavanaugh What I would like is basically syntax for this:

function is<T>(value: T): T { return value }

// Syntax for this:
is<Foo>(value) // upcast to `Foo`

Technically, this is a checked upcast, whereas as is an unchecked one (it can cast down as well as up). But this above is the semantics of any operator I'd prefer.

Just thought I'd clarify what I'm looking for.

Edit: I meant to include an apology for crossing up wires here - I've been busy at a conference and just typed that on the fly. This comment is much more precisely what I want.

RyanCavanaugh commented 6 years ago

The fact that you can write upcast<T>(x: T): T yourself but can't write check<T>(x extends T): typeof x (??) is another reason we want any hypothetical new operator to return the expression type rather than the check type.

ORESoftware commented 6 years ago

Yes, something like this: https://github.com/Microsoft/TypeScript/issues/27127

mpawelski commented 6 years ago

We punted a bunch of syntax ideas around but hated all of them.

Well, I don't know what kind of keyword team hated so I risk that I'll propose another one you'll hate 😨

I propose it to be assertis. I like it because it is short and has 'assert' inside and it's basically a compile-time assertion. It can only result in compile error, if there's no error then the result is the same as if we didn't use this language feature.

jack-williams commented 6 years ago

Some ramblings:

Possible names for SOMETHING: mustbe, satisfies, obeys.

Other idea: To avoid extending the expression syntax, put the static assertion on type annotations, rather than in expressions:

type HasM = { m: boolean };
type Neat = { [key: string]: boolean };
declare function fn(x: { m: boolean }): void;

const x: HasM mustbe Neat = {
    m: true
}; // x is type HasM

In the case we want inference, maybe omit or use *?

const x: * mustbe Neat = {
    m: true
}; 

// or

const x: mustbe Neat = {
    m: true
}; // x is type HasM

Then possibly allow chaining:

const x: HasM mustbe Neat, { x: true }, object = {
    m: true
}; // x is type HasM
simonbuchan commented 6 years ago

Flow uses simply : for a very similar feature they call casting for some bizarre reason. They require a wrapping () to avoid ambiguity, which I think is fine in this context, since you aren't going to want to double contextually type unless you're doing something real weird like type testing.

Another precedent is Haskell, which uses :: for the same thing.

fqborges commented 6 years ago

I would love this implemented to use with string literals, since this is valid but obviously wrong:

const x = { type: 'notanitem' as 'item', value: "example" }

If I have to write the string twice, at least the compiler should hint when I mess up.

// I vote for 'is'
const x = { type: 'item' is 'item', value: "example" }
// or flow style
const x = { type: ('item' : 'item'), value: "example" }
ajafff commented 6 years ago

@fqborges what you are describing in the first part of your post is also called "sidecasting" and was marked as "working as intended" in #16995. However, I've written a lint rule to detect this so you don't mess up: https://github.com/fimbullinter/wotan/blob/master/packages/mimir/docs/no-invalid-assertion.md

lonewarrior556 commented 5 years ago

SOMETHING:

mustbe, shouldbe, satisfies, satisfying obeys. matches, is, be, compares assertis complements, parallels, achieves , realizes, fulfils, withtype

There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton

DanielRosenwasser commented 5 years ago

I like satisfies or withtype.

TypeHintExpression ::    Expression withtype Type    Expression satisfying Type

eamodio commented 5 years ago

is is my pref and it has a nice corollary with as

RyanCavanaugh commented 5 years ago

I wanted to set aside the syntax conversation for a moment and collect some "real-world" use cases for where this operator would be useful.

e.g.

declare function paint(color: Color): void;

export type Color = { r: number, g: number, b: number };

// All of these should be Colors, but I only use some of them here.
// Other modules are allowed to use any of these properties.
// I have no way to find the typo here:
export const Palette = {
    white: { r: 255, g: 255, b: 255},
    black: { r: 0, g: 0, d: 0},
    blue: { r: 0, g: 0, b: 255 },
} /* ??op?? Record<string, Color> */;
//          ^^^ would cause the error to be identified

paint(Palette.white);
paint(Palette.blue);
// Correctly errors, but wouldn't if Palette were Record<string, Color>
paint(Palette.blur);
jack-williams commented 5 years ago

Heres a hacky way of doing it (error messages are poor).

export type Color = { r: number, g: number, b: number };

const check = <U>(x: U) => <T extends U extends T ? unknown : [U, 'not assignable to', T]>(): U => x;

export const Palette = check({
    white: { r: 255, g: 255, b: 255},
    black: { r: 0, g: 0, d: 0},
    blue: { r: 0, g: 0, b: 255 },
})<Record<string, Color>>();
fqborges commented 5 years ago

I wanted to set aside the syntax conversation for a moment and collect some "real-world" use cases for where this operator would be useful.

My main use case that appears a lot is in my example above (https://github.com/microsoft/TypeScript/issues/7481#issuecomment-438709565).

const x = { type: 'item', value: "example" }; // 'item' as 'item' literal not string

What I noticed is that in the latest versions of TypeScript most of the time when I just use the above it infers the literal type.

The second use case is just hint the type on the middle of an object, without being obliged to declare the the object as a whole. Example:

const myConfig = {
  name: 'foobar',
  uri: 'foo://bar',
  // ...
  // imagine a lot of props with distinct shapes here
  // ...
  palette: {
    white: { r: 255, g: 255, b: 255},
    black: { r: 0, g: 0, d: 0},
    blue: { r: 0, g: 0, b: 255 },
  } as Record<string, Color>
}
matt-tingen commented 5 years ago

Is a prefix operator an option? This would allow for intellisense to provide completions and avoid typos as you go.

RyanCavanaugh commented 5 years ago

Is a prefix operator an option? This would allow for intellisense to provide completions and avoid typos as you go.

We're a bit torn on this one. You're correct that putting the type before the expression is much better for completions, but at the same time we've been pushing people toward e as T for a while and it seems like a natural fit to have any new operation be of the same form. But if someone finds a really good prefix syntax, that would be a big win.

fqborges commented 5 years ago

Is the construction bellow possible without conflicting with current usings of <>?

const myConfig = {
  name: 'foobar',
  uri: 'foo://bar',

  <Record<string, Color>>palette: {
    white: { r: 255, g: 255, b: 255},
    black: { r: 0, g: 0, d: 0},
    blue: { r: 0, g: 0, b: 255 },
  }
}

Edited!

matt-tingen commented 5 years ago

Could you elaborate on the rationale for discouraging <T>e?

If the primary concern is with the parsing look-ahead when used with JSX, perhaps something like <:T>e could be used to keep the look-ahead at a constant 1 character peek. That notation is reminiscent of type annotations so it feels a bit more TypeScript-y to me.

fqborges commented 5 years ago

@matt-tingen If you are asking me, I am not against it. I just wrote faster than my head could elaborate my question/suggestion. 😅

matt-tingen commented 5 years ago

@fqborges I was meaning to ask @RyanCavanaugh. I feel like I remember seeing somewhere that the look-ahead added some complexity, but I wasn't sure if that was the main reason for encouraging e as T over <T>e. Understanding that would be beneficial if a new prefix syntax is to be considered.

RyanCavanaugh commented 5 years ago

e as T is the only one that works in TSX contests, so it's easier to just universally recommend that

jack-williams commented 5 years ago

Another use-case is being able to do richer excess property checking with helper types, without needing to keep the verbose helper type around:

type UnionKeys<T> = T extends unknown ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends unknown ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

const check = <U>(x: U) => <T extends U extends T ? unknown : [U, 'not assignable to', T]>(): U => x;

type Foo = {
    a : number,
    b : number,
};
type Bar = {
    a : number,
    c : number,
};

export const obj = check({
    a : 0,
    b : 0,
    c : 0,
})<StrictUnion<Foo | Bar>>(); // error
fefrei commented 4 years ago

Such an operator could also help resolve situations where type narrowing on initialization introduces a type error:

type A = 1 | 2;
const one: A = 1;
[one].concat([2]) // error!

The proposed operator allows the programmer to explicitly specify the exact type without giving up on type safety. See #36530 for details.

w0nche0l commented 4 years ago

I'm having a big need for something like this because a lot of Redux / React reducer hook patterns suggest to create an initial state literal, and then construct the state's type from that initial state.

For example, this is what I'm currently doing:

const initItemState = {
// the stuff inside here isn't important, it's just for demonstration purposes
    count: number,
    items: [] as SomeItem[],
    submissionStatus: {
        isSubmitting: false,
        errorMessage: ""
    } as GenericStatusInterface
}

type ItemState = typeof initItemState;

export function Reducer(
  state: ItemState,
  action: (actionType: string)=>ItemState
): ItemState { ... some function here}

However, if I later change SomeItem or GenericStatusInterface, I will not get an error because I am using as T.

A small workaround that I've found that decreases legibility/increases verbosity but does give errors is doing this:

const initItems: SomeItem = [];
const initSubmissionStatus : GenericStatusInterface= {
    isSubmitting: false,
    errorMessage: ""
}
const initItemState = {
    count: number,
    items: initItems,
    submissionStatus: {
        isSubmitting: false,
        errorMessage: ""
    } as GenericStatusInterface
}

I've only been able to casually skim the comments above, so if there has been any better workarounds for this, please let me know.