#Restricting property names in index signature based on inferred union

7 messages · Page 1 of 1 (latest)

onyx sparrow
#

I'm not sure how to best explain my problem…

I have this function that uses type inference to infer a union type from an array of strings:

enumerated({
  keywords: [ "", "one", "two", "three" ],
}) // ← returns a `ParseEnum<"" | "one" | "two" | "three">` using inference, works OK!

The options object can also take an aliases object whose keys must not be in the keywords, and values must be in the keywords.

enumerated({
  keywords: [ "", "one", "two", "three" ],
  aliases: { "empty": "", "un": "one" }, // ← valid, "" and "one" are valid keywords, and none of "empty" or "un" are keywords
}) // ← returns a `ParseEnum<"" | "one" | "two" | "three">` using inference, works OK!

enumerated({
  keywords: [ "", "one", "two", "three" ],
  aliases: { "un": "on" }, // ← invalid: "on" is not a keyword (here, detects a typo)
}) // ← returns a `ParseEnum<"" | "one" | "two" | "three">` using inference, works OK!

enumerated({
  keywords: [ "", "one", "two", "three" ],
  aliases: { "one": "" }, // ← invalid, "one" is a member of keywords
}) // ← returns a `ParseEnum<"" | "one" | "two" | "three">` using inference, works OK!

I can get the first two to work, but not the third.

I tried something like the following, which works for values, but not keys (Aliases is always inferred to string, and Exclude<> has no effect):

export function enumerated<Keywords extends string, Aliases extends string>(
  options: {
    keywords: Keywords[];
    aliases?: Record<Exclude<Aliases, Keywords>, NoInfer<Keywords>>;
  },
): ParseEnum<Keywords>;

I also tried variations putting the Exclude<> in the Aliases definition or using an index signature with remapping using Exclude<>.
And whereas I need to reference the Keywords type in the return type, Aliases is there only for type checking of the keys.

Now I would understand if this is not possible at all; all of this will be checked at runtime anyway, I'd just want to get a type checking error if possible.

sacred gale
#

You can use a validator type.

sleek auroraBOT
#
declare function fn<const T, U>(
    values: T[],
    aliases: {
        [K in keyof U]: K extends T ? never : U[K]
    },
): void

fn(['a', 'b', 'c'], {
    a: 'disallowed',
//  ^
// Type 'string' is not assignable to type 'never'.
    d: 'allowed',
})
onyx sparrow
#

Thanks
I'd swear I had tested this, but it indeed works. Error message is not really user-friendly so I'll have to ponder whether I prefer failing type checking with such a cryptic message or only fail at runtime (because I have runtime checks anyway). Here's what I tried that works (note: I actually have a type for the options):

export function enumerated<Keywords extends string, Aliases extends string>(
  options: {
    keywords: Keywords[];
    aliases?: {
      [Alias in Aliases]: Alias extends Keywords ? never : NoInfer<Keywords>;
    };
  },
): ParseEnum<Keywords>;
#

!resolved

sacred gale
sleek auroraBOT
#
declare function fn<const T, U>(
    values: T[],
    aliases: {
        [K in keyof U]: K extends T ? ['Error: this key already exists in values'] : U[K]
    },
): void

fn(['a', 'b', 'c'], {
    a: 'disallowed',
//  ^
// Type 'string' is not assignable to type '["Error: this key already exists in values"]'.
    d: 'allowed',
})