Machinarius
Machinarius

Reputation: 3731

How can I conditionally make an argument optional for a function in TypeScript?

I wrote a small script to generate all possible values from i18n templates into a TypeScript file to type-check my usages of the i18n function. I am struggling to figure out how to get TypeScript to play nice with strings that do not take any parameters in.

This is my current translation function definition:

import { I18nStrings } from "./i18nStrings";

export type CheckedTFunction = <TKey extends keyof I18nStrings>(
  key: TKey,
  options: I18nStrings[TKey]
) => string;

My i18nStrings.ts file looks like this (Small snippet for brevity, the real file is about 2500 lines long):

type I18nArg = string | number;
export type I18nStrings = {
"common:abbreviatedMonths.april": never,
"common:abbreviatedMonths.august": never,
"common:buttons.filtersApplied": { "count": I18nArg,  }, 
// ...
};

I incorrectly assumed that typing the args as never would allow TypeScript to be smart enough to figure out that the second parameter of the translation function should not be provided at all but it isn't the case: Linting (with tsc --build) my TypeScript files is raising errors for translation strings that don't take parameters, it expects the options parameter to be of any type.

Is what I am trying to do not possible? I am running TypeScript 4.2.3 at the moment.

Bonus points:

Linting does check that parameters are either strings or numbers with the right keys, but VS Code just reports the options argument as any. Is there something I can do to make VS Code smarter about this?

Edit: Based on @kaya3 's answer, this is what made it work:

export type CheckedTFunction = {
  <K extends KeysWithOptions>(key: K, options: I18nStrings[K]): string;
  <K extends KeysWithoutOptions>(key: K): string;
};

Upvotes: 2

Views: 332

Answers (2)

Aleksey L.
Aleksey L.

Reputation: 37928

You could define/type options as a rest parameter:

type I18nArg = string | number;

type I18nStrings = {
  "common:abbreviatedMonths.april": [],
  "common:abbreviatedMonths.august": [],
  "common:buttons.filtersApplied": [{ "count": I18nArg,  }], 
// ...
};

type CheckedTFunction = <TKey extends keyof I18nStrings>(
  key: TKey,
  ...options: I18nStrings[TKey]
) => string;

declare const check: CheckedTFunction;

check("common:abbreviatedMonths.april") // OK
check("common:buttons.filtersApplied") // Error: Expected 2 arguments, but got 1
check("common:buttons.filtersApplied", { count: 22 }) // OK

Playground

Upvotes: 2

kaya3
kaya3

Reputation: 51034

If you have a required argument of type never, that means your function can never be called.

What you want can be achieved with function overloads: have two overload signatures, one for the keys which require options, and one for the keys that don't take options. Here's a proof of concept:

type OptionTypes = {
    foo: {a: string},
    bar: {x: number, y: number},
    baz: never,
    qux: never,
}

type KeysWithoutOptions = {[K in keyof OptionTypes]: OptionTypes[K] extends never ? K : never}[keyof OptionTypes]
type KeysWithOptions = Exclude<keyof OptionTypes, KeysWithoutOptions>

function test<K extends KeysWithOptions>(key: K, options: OptionTypes[K]): void;
function test<K extends KeysWithoutOptions>(key: K): void;
function test<K extends keyof OptionTypes>(key: K, options?: OptionTypes[K]): void {
    console.log(key, options);
}

Tests:

// OK
test('foo', {a: 'foo'});

// error: Argument of type '{ x: number; y: number; }' is not assignable to parameter of type '{ a: string; }'.
test('foo', {x: 1, y: 2});

// error: Argument of type '"foo"' is not assignable to parameter of type 'KeysWithoutOptions'.
test('foo');

// OK
test('baz');

// error: Argument of type '"baz"' is not assignable to parameter of type 'KeysWithOptions'.
test('baz', {a: 'qux'});

Playground Link

Upvotes: 2

Related Questions