Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you specify an object literal's type in TypeScript?

Tags:

typescript

I'm wondering if there is a way to specify an object literal's type.

For example, how would one resolve this code and assign a B literal to an A variable:

interface A {
    a: string;
}

interface B extends A {
    b: string
}

const a: A = {
    a: "",
    b: ""
};

B is an A, so I expect to be able to assign a B where an A is expected. However, the compiler does not have enough context to figure out that I am passing a B as opposed to an illegal A, so I need to specify it.

I don't want to just make the above code compile, I specifically want my intent of "I am assigning a B to an A" to be conveyed to the compiler. I want the type safety of having a B object literal, so if B gets additional properties for example, this should fail to compile again.

I stumbled upon this post when searching for this. One option is to define an identity function as such:

const is = <T>(x: T): T => x;

const a: A = is<B>({
    a: "",
    b: ""
});

This is not ideal as it will run code for the sake of something that is irrelevant after transpiling. Another option is to use a second variable as such:

const b: B = {
    a: "",
    b: ""
};

const a: A = b;

Also not ideal for the same reason, it's code for the sake of a TypeScript check.

@jcalz's proposals:

interface A {
  a: string;
  [k: string]: unknown; // any other property is fine
}

This allows any excess properties to pass, when I just want A and its subtypes to pass. A's closedness is not the problem, I'm just unable to tell the compiler that I am passing a B where an A is expected.

Turn off --suppressExcessPropertyErrors compiler option

I want to suppress excess properties, the problem is that I'm unable to tell the compiler that these aren't excess properties, they are the properties of a proper subtype.

Is there an equivalent to type declarations for literals? Is there a fundamental design of TypeScript that prevents such a construct from existing?

In a language such as Kotlin, this is trivial:

val a: A = B()

You can separately declare the type of the variable and the type of the runtime object (must be the same type or a subtype).

In case this is an XY problem, the problem I was originally trying to solve looks like this:

const foo = (): A => {
    if (/* condition */) {
        /* some setup.. */
        return {
            a: "",
            b: ""
        }
    } else {
        return {
            a: ""
        }
    }
}

This does not compile because the first return is not explicitly an A, it is intended to be a B which is an A, but I have no way of specifying it as a B without resorting to one of the workarounds I mentioned above. I want something that looks like

return {
    a: "",
    b: ""
}: B

// or

return: B {
    a: "",
    b: ""
}

// or

return {
    a: "",
    b: ""
} is B

It goes without saying that as B type assertions are not what I'm looking for here. I don't want to bypass the type system, I just want to help the compiler check my object literal as a specified type. This problem also applies to parameters:

const foo = (a: A) => {};

foo({ a: "", b: "" })

This fails, because I have no way of telling the compiler that I am passing a B, it thinks I am using excess properties. It only works if I use an extra variable like so:

const b: B = { a: "", b: "" };

foo(b)
like image 462
user10271893 Avatar asked Jan 13 '20 23:01

user10271893


People also ask

How do I specify an object as a return type in TypeScript?

To declare a function with an object return type, set the return type of the function to an object right after the function's parameter list, e.g. function getObj(): {name: string;} {} . If the return type of the function is not set, TypeScript will infer it.

How do you define object of objects type in TypeScript?

To define an object of objects type in TypeScript, we can use index signatures with the type set to the type for the value object. const data: { [name: string]: DataModel } = { //... }; to create a data variable of type { [name: string]: DataModel } where DataModel is an object type.

Is object a data type in TypeScript?

The TypeScript object type represents any value that is not a primitive value. The Object type, however, describes functionality that available on all objects. The empty type {} refers to an object that has no property on its own.

How do you define a type of array of objects in TypeScript?

To declare an array of objects in TypeScript, set the type of the variable to {}[] , e.g. const arr: { name: string; age: number }[] = [] . Once the type is set, the array can only contain objects that conform to the specified type, otherwise the type checker throws an error. Copied!


1 Answers

Update: the main issue here seems to be that TypeScript does not have a "widening only assertion operator" or an "inline type annotation operator" (e.g., return {a: 123} op A should fail but return {a: "", b: ""} op A should succeed). There is an open suggestion for one, see microsoft/TypeScript#7481 but there's no real movement there. If you want to see this happen you might want to go to that issue and give it a 👍 and/or describe your use case if it's more compelling than what's already there. In the absence of an explicit type operator for this, there are strategies and workarounds available.


It sounds like you are already aware of the most common options for dealing with this, but you don't like any of them. I have a few other things you can try, but if your restrictions are:

  • no changes to your type definitions or annotations
  • no changes to the emitted runtime code
  • no type assertions

then your options are limited indeed and you might not find any answer satisfying.


Background on what's happening:

Object literals are an interesting exception to TypeScript's normal extendible object types. In general, object types in TypeScript are not exact. A type definition like {a: string} only requires that a value must have a set of known properties. It does not require that a value must lack unknown properties. So an object {a: "", b: 123, c: true} is a perfectly valid value of type {a: string}, because it has an a property whose value is assignable to string. So it seems that there should be nothing with your code; your a is indeed a valid A, with or without context.

But of course the compiler complains. That's because you've assigned a fresh object literal to a variable annotated with a type with fewer known properties than the object literal has. And thus it runs afoul of excess property checking, where object types are treated as exact. You can read the pull request implementing this feature and the rationale for doing this. Those issues also detail the suggested workarounds for situations in which the excess property checking is undesired.


You've already mentioned making a helper function (e.g., return is<A>({...})), assigning to an intermediate variable (e.g., const ret = {...}; return ret;), and using a type assertion (e.g., return {...} as A). Here are the other options as I see them:

  • Add an index signature you your A type so that it is explicitly open:

    interface A {
      a: string;
      [k: string]: unknown; // any other property is fine
    }
    
    const a: A = {  a: "", b: "" }; // okay now
    
  • Turn off excess property checking entirely with the --suppressExcessPropertyErrors compiler option (I don't really recommend this because it is such a wide-ranging change)

  • Use a union type like A | B instead of A when annotating the type corresponding to the object literal:

    const a: A | B = { a: "", b: ""  }; // okay now
    

    Since B is a subtype of A, the type A | B is structurally equivalent to A, so you can use it in place of A just about anywhere:

    const foo = (): A | B => { ... } // your impl here
    function onlyAcceptA(a: A) { }
    onlyAcceptA(foo()); // okay
    
  • use a type assertion just on the extra property, which requires using a computed property name:

     const a: A = { a: "", ["b" as string]: "" }; // okay
    

    That really isn't any less type safe, but it does end up generating slightly different JavaScript ({a: "", ["b"]: ""}), assuming you are targeting a version of JavaScript that supports computed property names.

Everything else I can think of just changes your runtime code more, so I'll stop there.


Anyway, hope that gives you some ideas. Good luck!

Link to code

like image 170
jcalz Avatar answered Oct 05 '22 22:10

jcalz