typescript - Infer Object Properties as Literals - Stack Overflow

admin2025-05-01  1

I am trying to infer all properties of an object as literals.

I know well that I can use the const assertion.

However, I don't want the resulting type to have readonly properties.

Here is the issue:

type Options = {
  min?: number;
};

class E1<O extends Options> {
  constructor(options: O) {}
}

const e1 = new E1({ min: 1 });

typeof e1 is:

E1<{
    min: number; // Oh no! This should be `1`
}>

Now, I know that I can use a const type parameter as described here: TypeScript 5.0 Docs.

Let's try it out:

class E2<const O extends Options> {
  constructor(options: O) {}
}

const e2 = new E2({ min: 1 });

typeof e2 is:

E2<{
    readonly min: 1; // Now it's `1`. Great! But it's `readonly`?
}>

The result I am desiring is this one:

typeof eN: (eN = "Example number N")

EN<{
   min: 1; 
}>

Now I have tried several solutions, of which none work:

class E3<const C extends Options> {
  constructor(c: { -readonly [K in keyof C]: C[K] }) {}
}

const e3 = new E3({ min: 1 });

typeof e3:

E3<{
    readonly min: 1;
}>

Or this one:

type NoRead<T> = {
  -readonly [K in keyof T]: T[K];
};

class E4<const C extends NoRead<Options>> {
  constructor(c: C) {}
}

const e4 = new E4({ min: 1 });

typeof e4:

E4<{
    readonly min: 1;
}>

There is one obvious solution to this:

class E5<C extends Options> {
  constructor(c: C) {}
}

const e5 = new E5({ min: 1 as const });

typeof e5:

E5<{
    min: 1;
}>

However, const assertions become unreasonable as the config object grows in size.

Now, I can already see the question: "Why would you want that?"

The answer is that it's mostly for IntelliSense purposes, to reduce visual load in more complex constructs of this kind.

I hope that is reason enough.

Summary: "I want to infer values of a generic as literals, while avoiding readonly modifiers."

Here is a Playground link: TS Playground.

I am trying to infer all properties of an object as literals.

I know well that I can use the const assertion.

However, I don't want the resulting type to have readonly properties.

Here is the issue:

type Options = {
  min?: number;
};

class E1<O extends Options> {
  constructor(options: O) {}
}

const e1 = new E1({ min: 1 });

typeof e1 is:

E1<{
    min: number; // Oh no! This should be `1`
}>

Now, I know that I can use a const type parameter as described here: TypeScript 5.0 Docs.

Let's try it out:

class E2<const O extends Options> {
  constructor(options: O) {}
}

const e2 = new E2({ min: 1 });

typeof e2 is:

E2<{
    readonly min: 1; // Now it's `1`. Great! But it's `readonly`?
}>

The result I am desiring is this one:

typeof eN: (eN = "Example number N")

EN<{
   min: 1; 
}>

Now I have tried several solutions, of which none work:

class E3<const C extends Options> {
  constructor(c: { -readonly [K in keyof C]: C[K] }) {}
}

const e3 = new E3({ min: 1 });

typeof e3:

E3<{
    readonly min: 1;
}>

Or this one:

type NoRead<T> = {
  -readonly [K in keyof T]: T[K];
};

class E4<const C extends NoRead<Options>> {
  constructor(c: C) {}
}

const e4 = new E4({ min: 1 });

typeof e4:

E4<{
    readonly min: 1;
}>

There is one obvious solution to this:

class E5<C extends Options> {
  constructor(c: C) {}
}

const e5 = new E5({ min: 1 as const });

typeof e5:

E5<{
    min: 1;
}>

However, const assertions become unreasonable as the config object grows in size.

Now, I can already see the question: "Why would you want that?"

The answer is that it's mostly for IntelliSense purposes, to reduce visual load in more complex constructs of this kind.

I hope that is reason enough.

Summary: "I want to infer values of a generic as literals, while avoiding readonly modifiers."

Here is a Playground link: TS Playground.

Share Improve this question asked Jan 2 at 17:44 Janek EiltsJanek Eilts 5825 silver badges12 bronze badges 8
  • 1 You can certainly do this in general, but not easily with a class declaration. If you're willing to write out the types like this playground link shows then does it meet your needs? If so I could write an answer explaining; if not, what's missing? – jcalz Commented Jan 2 at 17:54
  • I guessed it would be "impossible". I do like your approach! I modified it here. I have one issue, that is that I have this flag: exactOptionalPropertyTypes enabled in my project. That gives me an error, in my and your code. flag docs. I can't solve this with the Extract utility type. Any idea what is going on? Or is that a seperate issue? – Janek Eilts Commented Jan 2 at 19:48
  • It's definitely a separate issue (looks like ms/TS#60233) . You could try E<{ -readonly [K in keyof C]: C[K] } & Options>, or open a new question about it, but it's really not on topic for the question as asked. I'll write an answer given your original class thing (I mean, it's up to you if you want to turn it into a function, but that's outside the scope of the examples as written). – jcalz Commented Jan 2 at 20:09
  • Sure that is fine. The solution with the intersection does not quite work as desired: link. You can write your answer if you want to. – Janek Eilts Commented Jan 2 at 20:30
  • Oh, wow, actually this version might be the way to go. Your E3 was doing the reverse of what you wanted. You were saying c should be a non-readonly version of C, but it's actually the opposite. If that works better for you I'll write that as an answer. – jcalz Commented Jan 2 at 20:46
 |  Show 3 more comments

1 Answer 1

Reset to default 4

You can achieve this by using "reverse mapped types" (see this comment on microsoft/TypeScript#53018), also known as "inference from mapped types" (as documented in the deprecated TS Handbook, but not for some reason in the current one, even though nothing has changed about this).

That is, if you have a generic call or construct signature of the form function foo<T>(ft: F<T>): void then TypeScript can sometimes infer the type parameter T from a value of the mapped type F<T>. That's only possible when the mapped type F<T> is homomorphic (What does "homomorphic mapped type" mean?) in T. It's a "reverse" mapped type because, if you call foo(fa), TypeScript has to run the mapped type in reverse, getting the input T from the output F<T>.

For your example code, it would look like this:

class E<const C extends Options> {
  constructor(c: Readonly<C>) { }
}

Here we're still using a const type parameter C to encourage inference of literal types. But we're saying that the type of the constructor argument c is Readonly<C>, using the Readonly utility type (implemented as the homomorphic mapped type type Readonly<T> = { readonly [P in keyof T]: T[P]; }).

What it means is this: when you call new E(c), TypeScript will look at the type of c as Readonly<C>. In order to infer C from the type of c, it needs to reverse the operation of Readonly<>. Which means C is conceptually the non-readonly version of typeof c:

const e = new E({ min: 1, str: "abc" });
/* const e: E<{ min: 1; str: "abc"; }> */

And apparently that works. The const type parameter behaves as if you called new E({min: 1, str: "abc"} as const, which infers c as being of type {readonly min: 1, readonly str: "abc"}, and since that's Readonly<C>, TypeScript matches C with {min: 1, str: "abc"}. It's actually a bit surprising to me that this works, since it is arguably correct for C to still be {readonly min: 1, readonly str: "abc"}; since Readonly<C> would be the same either way. But luckily for you, TypeScript decides to actually apply the equialvent of the -readonly mapping modifier.


In the cases where reverse mapped types don't magically work, you'd need to do it yourself. That is, if new <C extends Options>(c: Readonly<C>) => E<C> wasn't doing it, then new <C extends Options>(c: C) => E<Mutable<C>> should do it, where type Mutable<T> = { -readonly [K in keyof T]: T[K] }>. Unfortuately TypeScript would never infer such a thing for a class declaration, meaning you'd need to write it out yourself manually and assign a constructor to it:

class _E<C extends Options> {
  constructor(c: C) { }
}
type E<C extends Options> = _E<C>;
const E: new <const C extends Options>(
  e: C) => E<{ -readonly [K in keyof C]: C[K] }> = _E;

And that gives the same result:

const e = new E({ min: 1, str: "abc" });
/* const e: E<{ min: 1; str: "abc"; }> */

This is more complicated though, so reverse mapped types are preferable when they work.

Playground link to code

转载请注明原文地址:http://anycun.com/QandA/1746104506a91729.html