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

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

Closed
magnushiie opened this issue Mar 11, 2016 · 90 comments · Fixed by #46827
Closed
Labels
Fix Available A PR has been opened for this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@magnushiie
Copy link
Contributor

magnushiie commented Mar 11, 2016

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
Copy link
Member

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

@magnushiie
Copy link
Contributor Author

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 DanielRosenwasser added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Mar 12, 2016
@DanielRosenwasser
Copy link
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
Copy link
Member

Sounds a lot like #2876?

@magnushiie
Copy link
Contributor Author

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
Copy link

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
Copy link
Contributor Author

magnushiie commented Sep 14, 2016

@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
Copy link

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
Copy link

normalser commented Sep 28, 2016

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
Copy link
Contributor

aluanhaddad commented Dec 29, 2016

@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
Copy link

I like this idea of effectively a static type assertion.

@magnushiie
Copy link
Contributor Author

magnushiie commented Mar 29, 2017

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

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Mar 29, 2017
@magnushiie
Copy link
Contributor Author

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

@jmagaram
Copy link

jmagaram commented May 2, 2018

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
Copy link
Member

@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
Copy link

jmagaram commented May 2, 2018

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
Copy link

ackvf commented May 16, 2018

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

@noppa
Copy link

noppa commented Jan 13, 2022

// What is the type of 'x' ?

If the type of x is not affected by satisfies and [1, 2] satisfies [number, number] becomes number[] again, then I guess you loose the ability to use this feature to annotate individual properties of objects? Expression level type annotations could be very handy to avoid boilerplate when you have lots of properties on an object and would rather not repeat them in the type definition and its implementation. I.e. I'd like the following to error, as it currently does in the playground

function createThing(str: string) {
    return {
        foo: [str] satisfies [string],
    }
}

createThing('foo').foo[1].toUpperCase()
//                     ^ Should not be allowed to access index 1 of [string]

// Is 'init' an excess property here?

Yes please. Guarding against excess properties (which are often typos, leftovers from old APIs or other mistakes) is a common need and being able to do it inside arbitrary expressions would be very handy.

Basically I was hoping just expression-level : , which I use quite often in Flow.
I guess this directly conflicts with @jtlapp 's use cases though.

// Is this a correct error?

Seems weird to me.
Why x = [1, 2] satisfies [number, number] is [number, number] but 3 satisfies 3 is not 3? 🤔

@Harpush
Copy link

Harpush commented Jan 13, 2022

I hope that's what this feature means - but a good scenario is combining as const with type annotation. Currently there is no way to say const a = {a: 2} as const satisfies Record<string, number> which in this case will make sure the object follows the record structure but at the end the type will be the actual const asserted type.

@noppa
Copy link

noppa commented Jan 13, 2022

@jtlapp If I understand your use-case correctly, you can already do something like

type ApiTemplate = {
  [funcName: string]: {
    call: (data: any) => void;
    handler: (data: any) => void;
  }
};

const apiConfig1 = {
  func1: {
    call: (count: number) => console.log("count", count),
    handler: (data: any) => console.log(data),
  },
  func2: {
    call: (name: string) => console.log(name),
    handler: (data: any) => console.log("string", data),
  },
}

// Currently working way to do type validation without any runtime impact
const _typecheck: ApiTemplate = null! as typeof apiConfig1

// TS can still autocomplete properties func1 and func2 here,
// and errors for missing "func". apiConfig1 is not merely "ApiTemplate".
apiConfig1.func

Playground.

The unfortunate thing about that approach is that it involves using an extra unused variable, which tends to make linters mad. satisfies would fix this, as you could instead do

// Assert that apiConfig1 is a valid ApiTemplate
null! as typeof apiConfig1 satisfies ApiTemplate

Playground

@jtlapp
Copy link

jtlapp commented Jan 13, 2022

Thank you, @noppa. I actually do have my code working, but it's missing an important piece, which it seems that I didn't overtly say in my use cases above:

I want VSCode highlighting the errors in the user's custom ApiTemplate definition, rather than at a later point in the code where the user passes that custom definition to some function.

The goal is to facilitate correct API development at the point where the API code is written.

I'm close to sharing my project to provide better clarity on the problem I'd like to solve. I just need to document it. I've found the Electron IPC mechanism so-many-ways error prone, and I've developed a way to create IPCs merely by creating classes of asynchronous methods, registering these classes on one end, and binding to them on the other. The entire objective is to make life as easy and bug-free as possible for the developer. I'm actually pretty amazed that I was able to do it. The only thing lacking at this point is having VSCode indicate API signature errors in the API code itself. I used to think the TypeScript type system was excessively complicated, but now I'm addicted to its power.

@jmroon
Copy link

jmroon commented Jan 16, 2022

The only way satisfies is useful to me is if it does provide additional context to the inferred type.

Use case: providing more accurate types for object literal properties. This is particularly useful in something like Vue Options API where everything is declared in an object literal.

@treybrisbane
Copy link

@RyanCavanaugh I took a look at your questions above, and these are my thoughts:


Example 1

const x = [1, 2] satisfies [number, number];
// What is the type of 'x' ?
// Argument for [number, number]:
//   If [1, 2] isn't a [number, number], then the 'satisfies' should have been rejected, but it wasn't
// Argument for number[]:
//   This operator is not supposed to change the type of the expression (except to remove implicit anys)

In this case, I'd expect the type of x to be [number, number], for two reasons:

  1. The type of the expression [1, 2] is number[], but since [1, 2] is assignable to type [number, number], and since [number, number] is more specific than number[], I expect the type of the overall expression to be [number, number]
  2. My mental model of the satisfies operator is essentially "assume at least this shape if (and only if) it's safe to do so", rather than "verify that it would be possible to assume this shape"

Example 2

type Disposable = {
    dispose(): void;
    beforeDispose?(): void;
};
const s = {
    init() {
        // Allocate
    },
    dispose() {
        // Cleanup
    }
} satisfies Disposable;
// Is 'init' an excess property here?
// Argument for:
//   If instead of 'init' we had written 'beforDispose' [sic],
//   failing to flag that as an error is a clear deficit
// Argument against:
//   This code sure looks fine. You could have written
//    } satisfies Disposable & Record<string, unknown>
//   instead if you were OK with excess properties

This is a tricky one.

At first I was going to make the argument that this case should raise an excess property error specifically because this code does:

const s: Disposable = {
  init() { ... },
  dispose() { ... }
};

However, I realised that such behaviour would contradict my mental model of the operator.

I mentioned above that my mental model of satisfies was essentially "assume at least this shape if (and only if) it's safe to do so". That "at least" bit is important, because it means that if the LHS is of a more specific type than the RHS (while still being assignable to the RHS), then I expect the overall type of the satisfies expression to be that of the LHS, not the RHS.
For example:

const value1 = (3 as const) satisfies number; // `value1` should be of type `3`
const value2 = ([1, 2, 3] as [1, 2, 3]) satisfies number[]; // `value2` should be of type `[1, 2, 3]`

So, revisiting the example from the question with this in mind:

type Disposable = {
  dispose(): void;
  beforeDispose?(): void;
};

const s = {
  init() { ... },
  dispose() { ... }
} satisfies Disposable;

I would expect the type of s to be

{
  init(): void;
  dispose(): void;
}

and because the type of s is not specifically Disposable, I would not expect to see an excess property error.


Example 3

let y = 3 satisfies 3;
let z: 3 = y;
// Is this a correct error?
//   Argument for: This operator is not supposed to change the types. You could have written 'as const' or 'as 3' instead.
//   Argument against: I literally just checked that this is 3!

I feel like this should not be an error.
My interpretation of this code is basically the same as the first example. The type of the expression 3 is number, but since 3 is assignable to type 3, and since 3 is more specific than number, I expect the type of the overall expression to be 3. As a result, I expect y to be of type 3, and thus its assignment to z to succeed.


I hope that helps! 🙂

Also, please let me know if my mental model of satisfies is incorrect in any way, as if it is, most of what I've said here will probably be invalid! 😁

@RyanCavanaugh
Copy link
Member

Also, please let me know if my mental model of satisfies is incorrect in any way

Right now there are no wrong answers -- the point of the exercise to determine what the "right" mental model of this operator should be!

@simonbuchan
Copy link

I would expect something to exactly like inference for generic constrained arguments, if that helps. If there's an obviously wrong behavior for one then it's probably also wrong for the other.

@kasperpeulen
Copy link

@simonbuchan I agree. I guess that you could model the spec to this function:

export function satisfies<A>() {
  return <T extends A>(x: T) => x;
}

If we then look @RyanCavanaugh examples, then it corresponds with @treybrisbane mental model:

/* ~~ Example 1 ~~ */
const x = satisfies<[number, number]>()([1, 2]);
// type is inferred as [number, number]

/* ~~ Example 2 ~~ */
type Disposable = {
  dispose(): void;
  beforeDispose?(): void;
};
const s = satisfies<Disposable>()({
  init() {
    // Allocate
  },
  dispose() {
    // Cleanup
  },
});
// Is 'init' an excess property here?
// No error here. And the type is inferred as: { init(): void, dispose(): void }

/* ~~ Example 3 ~~ */
let y = satisfies<3>()(3);
let z: 3 = y;
// There is no error, type of y is inferred as 3

Also the other examples:

const value1 = satisfies<number>()(3 as const); // `value1` is of type `3`
const value2 = satisfies<number[]>()([1, 2, 3] as [1, 2, 3]); // `value2` is of type `[1, 2, 3]`

// and another one
const value3 = satisfies<ReadonlyArray<number>>()([1, 2, 3] as const); // `value3` is of type `readonly [1, 2, 3]`

@simonbuchan
Copy link

@kasperpeulen thanks for the code and examples! Glad to see it works out, I wasn't on a computer and couldn't figure out what the satisfies function would look like.

@shicks
Copy link
Contributor

shicks commented Feb 1, 2022

I'd like to interject a slight tangent (pessimistically, scope creep?) that I haven't seen pointed out yet, but I think would be a good use case for any keyword introduced for these static checks. Specifically, some generic constraints are impossible to express due to circular references, and unlike top-level variable/expression type constraints, there's no other place to even put a dead assignment to get the check.

This has come up for me a number of times as a framework author wanting to type my APIs as tightly as possible to prevent accidental misuse. Most recently, I was trying to write an "isEnum" type guard (see also #30611):

function isEnum<T extends Enum<T>>(enumContainer: T): (arg: unknown) => arg is T[keyof T] { ... }
type Enum<T, E = T[keyof T]> =
    [E] extends [string] ? (string extends E ? never : unknown) :
    [E] extends [number] ? (
        true extends ({[key: number]: true} & {[P in E]: false})[number] ?
            unknown : never) :
    never;

Unfortunately, because of the [P in E] index required to work around #26362, TypeScript sees a circular reference and can't handle the T extends Enum<T>, but a human can look at this and see that this is not so much a type bound (that needs to be resolved in order to typecheck the body of the function), but rather a type constraint that only matters for checking callsites. This seems like an excellent use case for function isEnum<T satisfies Enum<T>> or function isEnum<T implements Enum<T>>, which would check the constraint for callers, but not have a problem with circularity.

The reason this is a good fit is that there's no other good place to do this check, due to how TypeScript does its type checking. In C++, you can write a static_assert in the body of the function, and since it re-typechecks each generic function with every new instantiation, it works. But TypeScript uses the template bounds to type check the function body once, and from then on callers are reduced to just checking the template bounds - so this allows more extensive verification that the types passed into the templates are actually reasonable.

FWIW, my current workaround is pretty ugly and doesn't work universally. It essentially boils down to

type NotAnEnum = {__expected_an_enum_type_but_got__: T};
type EnumGuard<T> = ... ? (arg: unknown) => arg is T[keyof T] : NotAnEnum<T>;
function isEnum<T>(enumContainer: T): EnumGuard<T> { ... }

This gets the job done, but as folks upthread have pointed out, it moves the error from the construction to the usage - because the isEnum function typechecks no matter what, it just returns an unusable value. This also relies on there being usable and unusable values: if the function returned void, for instance, this wouldn't work so well.

EDIT: added syntax highlighting

@kasperpeulen
Copy link

One other reason in favour of allowing satisfies to "assume at least this shape if (and only if) it's safe to do so".

When working with a lot of tuples, I often have the following problem:

declare function getTuple(): [number, number];
declare function calculate(tuple: [number, number]): number;
declare function memo<T>(factory: () => T): T;

function foo() {
  const [a, b] = getTuple();
  const tuple = memo(() => [b, a * 2]);
  // TS2345: Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
  // Target requires 2 element(s) but source may have fewer.
  return calculate(tuple); 
}

I think most people would first as const, but that gives another problem:

function foo() {
  const [a, b] = getTuple();
  const tuple = memo(() => [b, a * 2] as const);
  // TS2345: Argument of type 'readonly [number, number]' is not assignable to parameter of type '[number, number]'.   
  // The type 'readonly [number, number]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.
  return calculate(tuple); 
}

So now, what do you do? You have two options:

  • one is to change the parameter of calculate to be a readonly [number, number], which is fine, but also feels a bit arbitrary if you are not using readonly types in the rest of the project, and sometimes it means that many functions need to be refactored to readonly.

  • the other option is, to not use as const, but use as [number, number]. This seems like a solid solution, but it is actually quite dangerous, because what if now a refactor made getTuple to return declare function getTuple(): [number, number | undefined]; You may think that you could safely do this refactor in strict typescript, while my code stays sound, but no!

declare function getTuple(): [number, number | undefined];
declare function memo<T>(factory: () => T): T;
declare function calculate(tuple: [number, number]): number;

function foo() {
  const [a, b] = getTuple();
  // No error, but this code is not sounds anymore, silently converting number | undefined to a number
  const tuple = memo(() => [b, a * 2] as [number, number]);
  return calculate(tuple);
}

With satisfies, this could be done in a sound way:

declare function getTuple(): [number, number | undefined];
declare function memo<T>(factory: () => T): T;
declare function calculate(tuple: [number, number]): number;

function foo() {
  const [a, b] = getTuple();
  // This will not compile. The type need to be changed to [number | undefined, number] or the null value must be handled.
  const tuple = memo(() => [b, a * 2] satisfies [number, number]);
  return calculate(tuple);
}

@mbrowne
Copy link

mbrowne commented Feb 7, 2022

Here's another use case to consider:

import type { RequestHandler } from 'express'

interface RouteHandlers {
    login: RequestHandler
    logout: RequestHandler
}

export const routeHandlers: RouteHandlers = {
    login(req, res) {
        ...
    },

    logout(req, res, next) {
        ...
    }
}

satisfies would certainly make this more DRY:

export const routeHandlers = {
    login: ((req, res) => satisfies<RequestHandler>({
        ...
    })),

    logout: ((req, res) => satisfies<RequestHandler>({
        ...
    })),
}

...although in this case I can envision some syntax that would be even more concise, given that every value in the object should be a function of type RequestHandler. Obviously there are more important things than just being concise, just wanted to share the use case.

@cefn
Copy link

cefn commented Feb 10, 2022

To summarise what I recorded in the other issue (before I was helpfully referred to this feature discussion) and responding to @RyanCavanaugh ....

What is the type of 'x'

What I expected from a like operator - it would do nothing at all to the types of any existing language construction, but it WOULD raise compiler errors if the item cannot be known to satisfy the definition.

So for cases where the type IS satisfied it's a passthrough, and nobody has to reason about it at all, while if it ISN'T satisfied, then it would raise errors just like an assignment to a type (including excess property checks for literals) giving local and immediate feedback that it wasn't like the type you asked.

@RyanCavanaugh
Copy link
Member

I'd like to pick up the conversation at #47920 to get a clean comment slate. There's a large write-up there.

@spyro2000
Copy link

spyro2000 commented Mar 21, 2023

I would suggest something simple like

Case 1 ("strict cast"):

{prop: 123} as! MyInterface // fails on missing properties or properties not satisfying MyInterface

Case 2 ("safe cast")

{prop: 123} as? MyInterface // returns null if cast not successful

Case 3 ("unsafe cast")

{prop: 123} as MyInterface // just labels the object as the specified interface (currently the only possibility)

@microsoft microsoft locked as resolved and limited conversation to collaborators Mar 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fix Available A PR has been opened for this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet