6

The Typescript compiler will infer a string literal type for consts:

const a = 'abc';
const b: 'abc' = a; // okay, a is of type 'abc' rather than string

However, for properties, the type is inferred to be string.

const x = {
    y: 'def',
};

const z: { y: 'def' } = x; // error because x.y is of type string

In this example, how can I get the compiler to infer that x is of type { y: 'def' } without writing a type annotation for x?

Edit: There's an open issue requesting support for this feature. One suggested workaround is to use syntax like this:

const x = new class {
    readonly y: 'def';
};

const z: { readonly y: 'def' } = x; // Works

Try it in Playground here.

Edit 2: There's even an open PR that would solve this problem. Disabling type widening seems to be a popular request.

Sam
  • 1,260
  • 2
  • 11
  • 32
  • You can tell the compiler that something has specific type with [type assertion](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions): `const x = { y: 'def' as 'def', };` – artem Dec 02 '17 at 16:27

3 Answers3

5

I think you're looking for the const assertion, added in TS 3.4.

You just need to add as const to the string for it to become a literal type.

const x = {
    y: 'def' as const,
};

const z: { y: 'def' } = x; // no error :)

TS playground link

Daniel Reina
  • 5,764
  • 1
  • 37
  • 50
3

Yes, this issue (Microsoft/TypeScript#10195) is annoying for people who like to keep it DRY. As @artem mentions, you can do this:

const x = {
    y: 'def' as 'def'  // WET  
};
const z: { y: 'def' } = x; // okay

but that requires you to mention 'def' twice; once as a value, and once as a type. TypeScript can be forced to infer narrower types for type parameters in generic classes or functions, but not inside object literals.


However, if you're willing to live with a custom library and a bit more overhead, you can do this:

const x = LitObj.of('y', 'def').build(); // DRY
const z: { y: 'def' } = x; // okay

where LitObj is defined like so (preferably in its own module somewhere away from your code):

type Lit = string | number | boolean | undefined | null | {};
class LitObj<T> {
  obj = {} as T;
  private constructor() {
  }
  and<K extends string, V extends Lit>(k: K, v: V): LitObj<T & Record<K, V>> {
    var that = this as any;
    that.obj[k] = v;
    return that;
  }
  build(): {[K in keyof T]: T[K]} {
    return this.obj;
  }
  static of<K extends string, V extends Lit>(k: K, v: V): LitObj<Record<K,V>> {
    return new LitObj<{}>().and(k,v);
  }
}

The idea is that LitObj is a builder for literally-typed objects. At runtime it is just adding properties to an object, but the definition allows TypeScript to keep track of the literal key and value types. Anyway, hope that is helpful. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
2

The difference is there is no const keyword for properties. Since there is no way to be sure the properties won't be mutated TS cannot assume a constant string literal, it has to assume the more generic string.

Try replacing the first const in your example with let and at that location too TS is going to assume string and not 'abc':

let a = 'abc';
const b: 'abc' = a; 

TS Playground link for this code

Is going to show an error for b "Type string is not assignable to type 'abc'".

Since TS cannot infer immutability from a language feature, as you do in your const variables example, the only way is to tell it that the obejct property is immutable is via an explicit type annotation, meaning the answer to your question is a negative.

Mörre
  • 5,699
  • 6
  • 38
  • 63