Stephan D.
Stephan D.

Reputation: 326

Retrieve inferred string literal from object keys

I'm trying to create a type that defines the value based on the key. If the key extends $${string} (e.g. $foo) the value should be the key without the prefix e.g. foo. If the key doens't extend $${string} (e.g. boo) the values should be null.

Example

const example = {
  $foo: 'foo',
  boo: null,
}

Here is an isolated example I created to get it done - but it doesn't work as intended when I apply it to the code below. 😕

type Value<T> = T extends `$${infer I}` ? I : null

type ExampleA = Value<'$foo'> // type is 'foo' 
type ExampleB = Value<'boo'>  // type is null

My current code

type Values = {
    [K in string]: K extends `$${infer p}` ? p : null;
}

const values = {
    $foo: 'foo', // Type 'string' is not assignable to type 'null'.
    foo: null,
    
    $boo: 'boo', // Type 'string' is not assignable to type 'null'.
    boo: null,
} satisfies Values;

type Expected = {
    readonly $foo: 'foo',
    readonly foo: null,
    
    readonly $boo: 'boo',
    readonly boo: null,
}

The satisfies Values is used to infer the type later on. Similar approach is acceptable🙂

Thanks for your help and time - cheers

Upvotes: 1

Views: 106

Answers (1)

jcalz
jcalz

Reputation: 328453

The problem with your Values type is that mapped types over string do not behave the way you expect them to. While conceptually you can think of string as the infinite union of all possible string literal types, a mapped type over string does not even try to iterate over every possible string literal type; it just maps one thing: string:

type Values = {
  [K in string]: K extends `\$${infer S}` ? S : null;
}
/* type Values = {
    [x: string]: null;
} */

And since string does not extend `\$${infer S}`, then the property type for the string key is null.

This is working as intended, as discussed in microsoft/TypeScript#22509. Mapped types over string are not what you want.


And unfortunately there is no way to write a specific type in TypeScript which behaves the way you want. The closest you could get is something like

type Values = {
  [k: `\$${string}`]: string;
  [k: string]: string | null;
}

using a template string pattern index signature, but the parts where the property value string needs to match the part after the "$" character (not just string) and the part where other keys need to have a null (not just string | null) cannot be represented:

const values = {
  $foo: 'foo',
  foo: null,
  $boo: 'boo',
  boo: null,
  $oops: null, // error, not string
  oops: 'hmm', // should be error, but isn't!
  $whoops: 'oops', // should be error, but isn't!
} satisfies Values;

So we have to give up on the approach using the satisfies operator, because there is no appropriate Values type to use it with.


What you really care about is having the type of values inferred by the compiler but still checked against your desired constraint. We can get behavior like this by replacing satisfies Values with a generic helper function we can call satisfiesValues(). At runtime this function just returns its input, but the compiler can use it to validate the object literal passed in. So instead of const values = {...} satisfies Values; you would write const values = satisfiesValues({...});.

Here's one possible implementation:

const satisfiesValues = <K extends PropertyKey>(
  val: { [P in K]: P extends `\$${infer S}` ? S : null }
) => val;

The function is generic in K, the keys of the val value passed in. This will most likely be some union of known keys (none of which will be just string), and then the mapped type behaves as desired:

const values = satisfiesValues({
  $foo: 'foo',
  foo: null,
  $boo: 'boo',
  boo: null,
  $oops: null, // error, not "oops"
  oops: 'hmm', // error, not null
  $whoops: 'oops', // error, not "whoops"
});

/* const values: {
    foo: null;
    $foo: "foo";
    boo: null;
    $boo: "boo";
    oops: null;
    $whoops: "whoops";
    $oops: "oops";
} */

Looks good. The type of values is what you want it to be, and the compiler allows the valid properties and complains about the invalid ones.

Playground link to code

Upvotes: 2

Related Questions