Typescript: Operator to ensure an expression is contextually typed by, and satisfies, some type

Created on 11 Mar 2016  ยท  55Comments  ยท  Source: microsoft/TypeScript

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.

Needs Proposal Suggestion

Most helpful comment

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

All 55 comments

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

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.

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

image

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

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.

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
.

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
  }
});

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

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.

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

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.

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 (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>
}

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>>(); // error

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.

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 works
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antony-Jones picture Antony-Jones  ยท  3Comments

manekinekko picture manekinekko  ยท  3Comments

MartynasZilinskas picture MartynasZilinskas  ยท  3Comments

siddjain picture siddjain  ยท  3Comments

Zlatkovsky picture Zlatkovsky  ยท  3Comments