Preventing object literals type widening when passed as argument in TypeScript

646 Views Asked by At

Is is possible in the lastest TypeScript version to pass an object literal as argument to a function without widening it, and also without using as const in the invocation?

link to TS Playground: Example

What I currently do is this:

function func<T>(value: T): T { 
    return value;
};

let test = func({ key: 'value' })
// type inferred as { key: string;}

what I want is the following

// ... alternative declaration of func

let test = func({ key: 'value' })
// type inferred as { key: "value"; }

More precisely it should work for any object literal extending Record<string,string>

These archive the result I want, but Id like not to change the way the function must be invoked

function func<T>(value: T): T {
    return value
};

let test = func({ key: 'value' as const })
// type inferred as { key: "value"; }

let test = func({ key: 'value' } as const )
// type inferred as { readonly key: "value"; }

Is this possible?

2

There are 2 best solutions below

2
Tobias S. On BEST ANSWER

Yes, this is possible. But the solution might seem unintuitive and redundant.

You will have to add another generic type to the function. This will keep will allow us to keep narrowed type of string literals passed to the function.

function func<T extends Record<string, S>, S extends string>(value: T): T { 
    return value;
};

let test = func({ key: 'value', a: "a" })
// let test: {
//     key: "value";
//     a: "a";
// }

We can apply this to your complex example.

declare function formatMessage<
  Key extends keyof typeof messages, 
  Props extends { [key: string]: S }, 
  S extends string
>(messageKey: Key, props?: Props)
    :ReplaceValues<(typeof messages)[Key],  NonNullable<typeof props>>;

let test4 = formatMessage("created", {name: "TestValue"})
// let test4: "TestValue was created successfully."

Playground


Here are some further resources which helped me with problems like these in the past.

0
dsalex1 On

For anyone else coming around this question: ts-toolbelt has several utilities to help with this sort of type juggleing. In this case F.narrow is exactly what is needed to have the type be inferred unwidened, without the use of some sort of trick:

import { F } from 'ts-toolbelt'

function func<T>(value: F.Narrow<T>) { 
    return value;
};

Also coming out in TypeScript 5.0 the "const modifier" serves exactly this purpose of preventing type widening.