Home > front end >  How do I infer the exact type of an object function parameter in TypeScript?
How do I infer the exact type of an object function parameter in TypeScript?

Time:09-28

I have an object with properties that are typed using a generic template literal that requires a certain provided (as the generic type parameter) substring to be present in the string. Currently, if an invalid value is provided, the error is that the value cannot be assigned to the template literal.

I would like to make it so that the error message includes a type with custom message. E.g. Type 'never' is not assignable to type 'ErrorMessage<"You cannot do that.">'. I have seen libraries that do this, but I am not sure how to do it in this case. I have a generic that creates this, but it when trying get it all to work with objects, the properties are not narrow enough.

I have worked up to the following:

interface ErrorMessage<T> {
  __msg: T;
}

type Substring<
  str extends string,
  substr extends string
> = str extends `${string}${substr}${string}`
  ? str
  : ErrorMessage<`No match: '${str}' does not contain '${substr}'`>;

type Substrings = {
  bar: "aaa";
};

type Foo<T extends { [K in keyof Substrings]: string }> = {
  [K in keyof T & keyof Substrings]: Substring<T[K], Substrings[K]>;
};

type Infer<T extends { [K in keyof Substrings]: string }> = {
  [K in keyof T]: T[K];
};

const helper =
  <T extends { [K in keyof Substrings]: string }>(val: Infer<T>) =>
  (val2: Foo<T>) =>
    val2;

const foo = helper({
  bar: "has bar aaa",
})({
  bar: "has bar aaa",
});

This works in theory, but the duplicate function call is awkward. Is there any way I can make helper "self-contained"? Every other variation I have tried ends up with the property types of the object being inferred as just string. The main issue being that TypeScript can't infer T for helper when using a generic that otherwise works.

I could pass the type parameter, but that requires defining the object separately as const and then using typeof, which is also quite awkward.

As a slightly different approach, the following almost works, except for K not being a valid index of Substrings in the mapping in Foo:

type Foo<T extends { [K in keyof T & keyof Substrings]: string }> = {
  [K in keyof T]: Substring<T[K], Substrings[K]>;
};

const helper = <T extends { [K in keyof Substrings]: string }>(val: Foo<T>) =>
  val;

I may have gone up a dead end with this particular approach. If you know or can figure out a different way, I'm happy with that. I have no special affinity for what I've got at the moment, it's just what I've ended up on so far.

I have a feeling there will be some function overloads and currying involved, but I'm not proficient enough to implement it.

Thanks in advance.

Update: plain example with regular error messages:

type Substring<substr extends string> = `${string}${substr}${string}`;

type Substrings = {
  bar: "aaa";
};

type Foo<T extends { [key: string]: string }> = {
  [K in keyof T]: Substring<T[K]>;
};

const helper = <T extends Foo<Substrings>>(val: T) => val;

const foo = helper({
  bar: "has bar aaa",
});

CodePudding user response:

Here's one possible approach:

const helper = <T extends Record<keyof Substrings, string>>(
    t: { [K in keyof T]: K extends keyof Substrings ? (
        T[K] extends `${string}${Substrings[K]}${string}` ?
        T[K] : ErrorMessage<`No match: '${T[K]}' does not contain '${Substrings[K]}'`>
    ) : T[K] }
) => t;

The algorithm the type checker uses when inferring generic type parameters is a bit fiddly. The compiler is pretty good about inferring from homomorphic mapped types, meaning that it can often infer T from the type {[K in keyof T]: ...T[K]...}. But the particular place T[K] appears in that ...T[K]... has a lot to do with how successful the inference will be.

One rule of thumb I use is that if you want the compiler to infer a generic X from a conditional type, you should make sure that X appears directly as the checked type, like X extends ... ? ... : .... Since we want the compiler to infer T[K], then we want T[K] extends ... directly. Hence the T[K] extends `${string}${Substrings[K]}${string}` ? ... type. You might be able to refactor this into its own named type alias, but this could have strange add-on effects I don't want to get into here.


Anyway, let's test it:

const foo1 = helper({
    bar: "has bar aaa",
}); // okay

const foo2 = helper({
    bar: "oopsie doodle" // error!
    //~ <-- 'ErrorMessage<"No match: 'oopsie doodle' does not contain 'aaa'">'
})

Looks good. You get the error message you're looking for.

Playground link to code

  • Related