Operator to ensure an expression is contextually typed by, and satisfies, some type
magnushiie opened this issue ยท 90 comments
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.
Can you post a few examples of how you'd like this to work so we can understand the use cases?
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);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".
Sounds a lot like #2876?
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 -> natHere, 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.
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.
@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.
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;
});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@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.
I like this idea of effectively a static type assertion.
@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.
Also linking #13788 about the current behavior of the type assertion operator as being both upcast and downcast.
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);
@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?
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}));
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.
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
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.
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.
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.
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
}
});
@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.
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?
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);@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
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!
@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.
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.
Yes, something like this:
#27127
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.
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 HasMIn 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 HasMThen possibly allow chaining:
const x: HasM mustbe Neat, { x: true }, object = {
m: true
}; // x is type HasMFlow 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.
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" }
@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
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
I like satisfies or withtype.
TypeHintExpression ::
Expression withtype Type
Expression satisfying Type
is is my pref and it has a nice corollary with as
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);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>>();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 (#7481 (comment)).
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>
}
Is a prefix operator an option? This would allow for intellisense to provide completions and avoid typos as you go.
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.
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!
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.
@matt-tingen If you are asking me, I am not against it. I just wrote faster than my head could elaborate my question/suggestion. ๐
@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.
e as T is the only one that works in TSX contests, so it's easier to just universally recommend that
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>>(); // errorSuch 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.
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.
We ran into a situation like this a little while back.
We've got this type:
type Thing = {
[key: string]: Thing | number
};
and this object:
const a = {
foo: 4,
bar: {
a: 2,
b: 3
}
};
We want to assert that a has the structure of Thing, but without erasing the properties of a (that is, we would want a.c to result in a type error).
The solution we ended up using was
const makeThing = <T extends Thing>(x: T): T => {
return x;
};
const a = makeThing({ ... });
It would be nice to have something like const a = { ... } is Thing instead.
This would be helpful in situations like this with unions of promises of different types:
interface A { a: number }
interface B { b: number }
declare const p1: Promise<A>
declare const p2: Promise<B>
// This expression is not callable.
// Each member of the union type '...' has signatures, but none of those signatures are
// compatible with each other.
(Math.random() > 0.5 ? p1 : p2).then(() => {})
// Safe, but must use another variable
const safe: Promise<A | B> = Math.random() > 0.5 ? p1 : p2
safe.then(() => {})
// No extra variable/function required, but unsafe
;((Math.random() > 0.5 ? p1 : p2) as Promise<A | B>).then(() => {})
// Could fail at runtime
;((Math.random() > 0.5 ? p1 : p2) as Promise<A>).then(x => console.log(x.a.toFixed()))I'm working on something where this would be very useful to have. I was hoping there would be a symbol to reference the type itself, like & or @ or : ๐คทโโ๏ธ. Just posting an example of what's in my head in case it helps:
type StateMachine = {
startAt: keyof &.states,
states: {
[key: string]: State
}
}@ff-jkrispel There's this, but that only works in interfaces. And I've used that quite a bit to do advanced type hackery with records without requiring spraying additional generic parameters everywhere.
@DanielRosenwasser The is operator described above would respond to the issue that I raised #41715 (provided it can be used along the as const statement). You can close it.
I'll add my use case for the record ๐
I have developed a library that infers valid types from JSON schemas only through type computations, by making profit of the as const statement, generics and type recursions ๐ https://github.com/ThomasAribart/json-schema-to-ts
import { FromSchema } from "json-schema-to-ts";
const dogSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" },
hobbies: { type: "array", items: { type: "string" } },
},
required: ["name", "age"],
additionalProperties: false,
} as const;
type Dog = FromSchema<typeof dogSchema>;
// => { name: string; age: number; hobbies?: string[] }The as const statement is needed to keep typeof dogSchema as narrow (and thus parsable) as possible.
Currently, FromSchema will raise an error if typeof dogSchema doesn't extend DefinitelyTyped's JSON schema definitions. It's better than nothing, but it would be great to have the benefits of a type assignment (autocomplete, error highlighting etc...).
import { JSONSchema } from "json-schema-to-ts"; // <= Readonly type based on DefinitelyTyped
const invalidSchema = {
type: "object",
properties: {
name: "string" // <= Error is raised
} as const is JSONSchema;
const validSchema = {
type: "object",
properties: {
name: { type: "string" },
}
} as const is JSONSchema;
type ValidData = FromSchema<typeof validSchema> // <= Still worksThis would be very helpful. I'm currently using a helper function
function isType<T>(value: T): T {
return value;
}
in my source code, but this obviously creates an unnecessary function call.
Hi, I appear to have written a sister issue to this one #42138.
The way I see it, it feels to me this suggested operator might be built around the workaround rather than address the root issue at hand: that there exist currently untypable constructs in Typescript (i.e. variables that cannot be currently typed-checked without so called "utility-functions" like the one shown in the workarounds above).
Out of the top of my head it occurs to me that there are at least four scenarios of untypable constructs :
- Objects
- Arrays
- Generics
- Functions
I believe that these scenarios, are what you are trying to solve with this operator and that they could also be addressed with contextual types.
Proposals
These proposals all specify contextual types using the keyword infer used outside of conditional types. I will use string for all non-contextual types provided by the user.
Objects
// LHS, Infer keys types
type ValuesMustBeStrings = {[key: infer] : string}
// RHS, Infer value types
type KeysMustBeStrings = {[key: string] : infer}This would be very useful for declaring collections objects with constrains that need to be followed. The two main work arounds for this are index types, or as const which are not meant to be used used for this use-case and have undesirable side-effects (e.g. forcing you to hardcode keys and values and making everything readonly respectively).
Arrays
// Infer value types
type LengthTwoTuple = [infer, infer]
type FirstValueMustBeAString = [strings, โฆinfer]This would be very useful for tuple types, as the current way of typing an inferred tuple is through hacky typing-functions.
Generics
// Assume a type from a third party library with three generic argument A,B and C
const myArglessThing: TypeFromLibrary = new Thing();
const myExplicitlyArglessThing: TypeFromLibrary<infer, infer, infer> = new Thing();
//Assume now that the default values are unsuitable and I donโt want override inference of the last two types.
// Either because I donโt want to or canโt easily do so(say the default types for the the second two values are private)
const myThing: TypeFromLibrary<string, infer, infer> = new Thing(โlets say this is the string valueโ);Functions
type Both_inferred = infer => infer // this would be the explicit equivalent to not providing a type on declaration
type Lhs_inferred = infer => number // only the argument is inferred
type Rhs_inferred = ( arg: number ) => infer // only the return type is inferredSee: #42138 where I wrote extensively about this functions with contextual typings.
@RodrigoRoaRodriguez I have read your comment twice, I don't have the feeling that it has anything to do with this issue. Perhaps you posted it at the wrong place.
It is not only me. Others seem to think #42243 is related to #7481 (see #42243). While superficially the two issues might seem unrelated, they seek to solve the same problem.
Look at the my examples of function workarounds, and you will realize that they are all either variants of, or identical to:
function asType<T>(value: T) {
return value;
};In fact, just like in the third post in this thread, my own code example demo for #42243 was also state management in React with component actions.
The main difference between the two Github issues is that while #7481 suggests an operator:
// I know this is the workaround, but I could not find a proposal for how the operator syntax is supposed to look
export const ConnectedAsType = connect(undefined, asType<MyComponentActions>({
selectSomething,
}))(MyComponent);#42243 suggests a type as a solution:
const Actions: TypeWithFunctionInference = {
selectSomething,
};
export const ConnectedAsType = connect(undefined, actions)(MyComponent);As you can see, both seek achieve the same result. #7481 and #42243 are just approaching a common problem from different angles and suggesting different syntax.
This is a complex issue and one that I understand it is difficult to explain, given how abstract and large it is. Please, do not hesitate to ask if you have any questions or would like a more in-depth explanation of what I am saying and why I am saying it.
Looks like @ThomasAribart beat me to my own suggestion. That being said, if i may offer one suggestion on top of his - I think there's a big opportunity here to re-use the implements keyword rather than creating a new is operator:
interface JSONSchema { /* ... */ }
const validSchema = { /* ... */ } as const implements JSONSchema;or even
interface JSONSchema1 { /* ... */ }
type JSONSchema2 = { /* ... */ }
const validSchema = { /* ... */ } as const implements JSONSchema1, JSONSchema2;Forgive me now for scope-creeping, but if we entertain the idea of a generalized implements, this could be used in a much more powerful way with other variations of type declarations (see related suggestion):
// explicit
type ValidSchema implements JSONSchema1, JSONSchema2 = { /* ... */ }
type ValidSchema = { /* ... */ } implements JSONSchema1, JSONSchema2
interface ValidSchema implements JSONSchema1, JSONSchema2 { /* ... */ }
// inferred
const validSchema implements JSONSchema1, JSONSchema2 = someExpression
const validSchema = someExpression implements JSONSchema1, JSONSchema2I recently posted another use case to StackOverflow. I'm generating an API from a structure and need the structure to have a certain conformation while still preserving its unique type.
All I'd like is a declarative shorthand for the following function:
function assertTemplate<T extends Template>(template: T) {
return template;
}Maybe something like:
const api: ConformsTo<Template> = {
// ...
};Which would be equivalent to:
const api = assertTemplate({
// ...
});Most of the time I tend to do something like this:
export const STRING_CONVERT = {
num: (num: number) => num.toString(),
bool: (bool: boolean) => (bool ? "yes" : "no"),
};
// Check that STRING_CONVERT only includes functions that return strings.
STRING_CONVERT as Record<string, (v: unknown) => string>;I know this leaves a random STRING_CONVERT; in the code but tree shaking cleans it up (maybe?). Flow has assertion syntax (using :) that does get removed from output so I started doing something similar when I switched to TS.
But I agree the following with implements is much cleaner (and a great suggestion!):
export const STRING_CONVERT = {
a: (num: number) => num.toString(),
b: (bool: boolean) => (bool ? "yes" : "no"),
} implements Record<string, (v: unknown) => string>;@dhoulb Your example highlights why as and its downcasting behaviour is bad at doing assertions like that
export const STRING_CONVERT = {
// woops, the actual return type here is 0 | string
num: (num: number) => num && num.toString(),
};
// Passes type check without errors :(
STRING_CONVERT as Record<string, (v: unknown) => string>;whereas
const _check: Record<string, (v: unknown) => string> = STRING_CONVERT;would give you an error as expected.
I recently posted another use case to StackOverflow. I'm generating an API from a structure and need the structure to have a certain conformation while still preserving its unique type.
All I'd like is a declarative shorthand for the following function:
function assertTemplate<T extends Template>(template: T) { return template; }Maybe something like:
const api: ConformsTo<Template> = { // ... };Which would be equivalent to:
const api = assertTemplate({ // ... });
I usually have a use case very similar to @jtlapp, but I think it's specially relevant when there are generics in between.
Simplified example, I have an interface with a generic:
interface ColumnDefinition<M, T> {
id: string,
selector: (model: M) => T,
renderer: (value: T) => JSX.Element
}If I want to instantiate a variable with this type, I have to put the generics in:
const labelColumn: ColumnDefinition<Todo, string> = {
id: 'label'
selector: todo => todo.label,
renderer: label => ...
}and I can't avoid it directly. If I put in any for the generics (or default them), then renderer will have "label" as any too. I usually have the same identity function that does the inference for me:
function colDef<M, T>(columnDefinition: ColumnDefinition<M, T>) {
return columnDefinition
}
const labelColumn = colDef({
id: 'label'
selector: (todo: Todo) => todo.label,
renderer: label => ... // T inferred nicely as string
})I have a situation for which I haven't found a workaround. I want each element of an array to conform to the same abstract structure without requiring all the elements to be of the same type. I can't use classes because I'm dictating that all object members have a certain structure, regardless of their names.
It seems that the ConformsTo<T> proposal would address this:
function processRecords(records: ConformsTo<RecordStructure>[]) {
...
}The problem is that each element of the array can have a different set of properties.
UPDATE. Here's a workaround, but I think it's too awkward to use in a utility API:
type CheckedApi = { [key: string]: (...args: any[]) => Promise<any> };
function checkApi<T extends ServerApi<T>>(api: T) {
return api as CheckedApi;
}
class Api1 {
async func1() {}
}
class Api2 {
async func2() {}
}
function exposeApis(_apis: CheckedApi[]) {}
const api1 = new Api1();
const api2 = new Api2();
exposeApis([checkApi(api1), checkApi(api2)]);I think #33480 (a proposal for referencing the inferred type on the LHS) could also address many of the use cases here
I would like to point out yet another workaround, It works in current TypeScript v4.5.2.
type Same<P,T> = P extends T? (T extends P? 1: 0): 0
const verifySame = <P> () => <T> (_: Same<P,T>, t:T): T => t
type Hello = {hello: string}
verifySame<Hello>()(1, {hello: "world"})
verifySame<Hello>()(0, {hello: "world", since : 2020})
//this will not compile:
//Argument of type '1' is not assignable to parameter of type '0'.ts(2345)
//verifySame<Hello>()(1, {hello: "world", since : 2020})verifySame has a higher rank type but appears to be supported by TS.
verifySame should provide type safety preventing subtyping.
To clarify my comment, the rank-2 is convenient in some case but often not needed. Cleaning up my above example (replacing literal numbers with literal booleans) here is a safe invariant version of the array push:
//Cleaner version of the above 'Some' uses literal booleans
type Same<P,T> = P extends T? (T extends P? true: false): false
//Note to get safety I end up with casting, this is quick and dirty but still ...
const safePush = <P, T> (_: Same<P,T>, ps: P[], t: T): number => ps.push(t as any)
const intlist: number[] = [1,2,3]
const list: unknown[] = intlist //exploits array covariance
list.push("not a number") //TS push adds 'string' to 'intlist'
safePush(true, intlist, 1) //this is safe
//safePush(true, list, 1) //this is risky and will not compile
//safePush(true, list, "not a number") //this is risky (here wrong) and will not compileThis does not use rank-2 tricks. It shows how such work-around could be used in APIs.
I have some open questions based on playing with the PR and would like to hear people's 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)
/* ~~ 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
/* ~~ 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!My understanding was that the purpose of satisfies was not just to check for assignability, but also to trigger contextual typing, so I donโt find the fact that it changes the inferred type from array to tuple surprising. Itโs quite useful that way, as it can do what would have required a constrained identity function without it.
I would find the operator much less useful if it only filled in implicit anys. One of the use cases that immediately came to mind when I found out about this change was this:
const foo = null satisfies string | null;
// instead of:
// const foo: string | null = null as string | nullโฆwhich wouldnโt work if satisfies couldnโt change the inferred type.
Relatedly, I would also intuitively expect cat satisfies Animal to be an upcast.
const foo = null satisfies string | null;
Based on prior discussion, I don't think we want foo to be string | null here. The reason is that, using the example from my prior comment, you want to be able to call s.init() -- this implies we should use the expression type, not the asserted type.
I was assuming that init in that example was going to be treated as an excess property, consistent with satisfies being a contextual-typing hint.
I guess I find any solution where satisfies canโt change the type of an expression to be potentially confusing, since the type of an expression is usually affected by contextual typing anyway. It just seems natural that โthe contextual typing operatorโ should be able to change the type of an expression. Itโs more mental overhead if I have to remember that satisfies contextually types an expression, but doesnโt affect its type like other cases of contextual typing do. TypeScript IMO has way too many of these special cases already.
I'm here hoping for a conforms-to assertion for ensuring that multiple different types conform to the same type and still remain their respective different types after the assertion. Changing the type completely undermines my scenarios, described above here and here. Please let me know if I posted to the wrong issue, as type-preservation is fundamental to what I need to accomplish.
// 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? ๐ค
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.
@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.funcThe 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 ApiTemplateThank 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.
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.
@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:
- The type of the expression
[1, 2]isnumber[], but since[1, 2]is assignable to type[number, number], and since[number, number]is more specific thannumber[], I expect the type of the overall expression to be[number, number] - My mental model of the
satisfiesoperator 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! ๐
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!
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.
@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 3Also 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]`@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.
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
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
calculateto be areadonly [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 useas [number, number]. This seems like a solid solution, but it is actually quite dangerous, because what if now a refactor made getTuple to returndeclare 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);
}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.
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.
I'd like to pick up the conversation at #47920 to get a clean comment slate. There's a large write-up there.
I would suggest something simple like
Case 1 ("strict cast"):
{prop: 123} as! MyInterface // fails on missing properties or properties not satisfying MyInterfaceCase 2 ("safe cast")
{prop: 123} as? MyInterface // returns null if cast not successfulCase 3 ("unsafe cast")
{prop: 123} as MyInterface // just labels the object as the specified interface (currently the only possibility)


