Specializing in narrowing types with two generic parameters

In my current project, I am working on a function that takes two generic parameters: "index" which is a string and "language" which can also be any string. The goal of the function is to validate if the given language is supported and then return a formatted string in the form of

${index or T}_${one of SupportedLanguages}
.

So far, I have managed to ensure that the function works correctly, for example:

const res = getIndexWithPrefixByLanguage('haha') // returns type 'haha'
const res2 = getIndexWithPrefixByLanguage('haha', 'ja') // returns type 'haha_ja'

The issue I am facing now is narrowing down the second parameter U to one of the supported languages. It seems like the problem lies within the return type signature.

Below is the full code snippet:

export type SupportedLanguage = 'ko' | 'ja' | 'zh'
export const SUPPORTED_LANGUAGES = ['ko', 'ja', 'zh']
interface IndexMap<T extends string> {
  ko: T
  ja: `${T}_ja`
  zh: `${T}_zh`
}

export function getIndexWithPrefixByLanguage<
  T extends string,
  U extends string,
>(index: T, language?: U): IndexMap<T>[U extends SupportedLanguage ? U : 'ko'] {
  if (!validateLanguage(language)) {
    return index
  }
  console.log(language)
  const map: IndexMap<T> = {
    ko: index,
    ja: `${index}_ja`,
    zh: `${index}_zh`,
  }

  // how to narrow down to one of Supported Language type?
  return map[language]

}

export function validateLanguage(
  language?: any,
): language is SupportedLanguage {
  return !!language && SUPPORTED_LANGUAGES.includes(language.toLowerCase())
}

When trying to compile the code, I encounter the following error:

Type 'IndexMap<T>[U & "ko"] | IndexMap<T>[U & "ja"] | IndexMap<T>[U & "zh"]' is not assignable to type 'IndexMap<T>[U extends SupportedLanguage ? U : "ko"]'.
  Type 'IndexMap<T>[U & "ja"]' is not assignable to type 'IndexMap<T>[U extends SupportedLanguage ? U : "ko"]'.
    Type 'U & "ja"' is not assignable to type 'U extends SupportedLanguage ? U : "ko"'.ts(2322)

It appears that I need to find a way to narrow down this to

U extends SupportedLanguage ? U : "ko"

Is there a solution to achieve the desired functionality without resorting to type assertion?

Answer №1

To begin, let's focus on the SUPPORTED_LANGUAGES variable.

The key is to ensure that you have a single source of truth. This means that the SupportedLanguage definition should be updated:

export const SUPPORTED_LANGUAGES = ['ko', 'ja', 'zh'] as const
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]

By using the as const assertion, we can infer a literal type.

The next step is to refactor the validateLanguage function. While it should ideally be a custom typeguard, in this case, due to the use of Array.prototype.includes, we need to implement an additional trick:

const withTuple = <
  List extends string[]
>(list: readonly [...List]) =>
  (prop: string): prop is List[number] =>
    list.includes(prop)

const validateLanguage = withTuple(SUPPORTED_LANGUAGES);

I had to curry this typeguard, but now the code is free of TypeScript errors and avoids the use of any. For more patterns like this, check out my article on TypeScript useful patterns.

Regarding the usage of conditional types within return types, this feature is not directly supported in TypeScript. To achieve this, you must overload your function and then utilize conditional typings.

Below is the complete code snippet:


// Code continues... (Content truncated for brevity)

Explore the full functionality in this interactive Playground.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Achieving TypeScript strictNullChecks compatibility with vanilla JavaScript functions that return undefined

In JavaScript, when an error occurs idiomatic JS code returns undefined. I converted this code to TypeScript and encountered a problem. function multiply(foo: number | undefined){ if (typeof foo !== "number"){ return; }; return 5 * foo; } ...

Theme not being rendered properly following the generation of a dynamic component in Angular

I am currently working on an Angular 9 application and I have successfully implemented a print functionality by creating components dynamically. However, I have encountered an issue where the CSS properties defined in the print-report.component.scss file a ...

Is there a lack of compile checking for generics in Typescript?

Consider the code snippet below: interface Contract<T> { } class Deal<D> implements Contract<D> { } class Agreement<A> implements Contract<A> { } Surprisingly, the following code compiles without errors: let deal:Contract ...

Extending Mongoose's capabilities with header files for the "plugin" feature, utilizing the .methods and .statics methods

My task is to develop Typescript header files for a script that enhances my Mongoose model using the .plugin method. The current signature in the Mongoose header files looks like this: export class Schema { // ... plugin(plugin: (schema: Schema, opt ...

The TS2345 error is triggered when using the fs.readFile function with specified string and

Attempting to utilize the fs.readFile method in TypeScript, my code looks like this... import {readFile} from 'fs'; let str = await readFile('my.file', 'utf8'); This results in the following error message: TS2345: Argumen ...

Ensuring a child element fills the height of its parent container in React Material-UI

Currently, I am in the process of constructing a React Dashboard using MUI. The layout consists of an AppBar, a drawer, and a content area contained within a box (Please correct me if this approach is incorrect)... https://i.stack.imgur.com/jeJBO.png Unf ...

Creating a React component in Typescript to utilize lodash helper functions

I have a component that looks like this: import React, { Component } from 'react'; import throttle from 'lodash.throttle'; interface Props { withScroll: boolean; } class Image extends Component<Props, {}> { throttledWindowS ...

What is the best method to extract the values of objects in an array that share

var data= [{tharea: "Rare Disease", value: 3405220}, {tharea: "Rare Disease", value: 1108620}, {tharea: "Rare Disease", value: 9964980}, {tharea: "Rare Disease", value: 3881360}, ...

Unsure how to proceed with resolving lint errors that are causing changes in the code

Updated. I made changes to the code but I am still encountering the following error: Error Type 'String' is not assignable to type 'string'. 'string' is a primitive, but 'String' is a wrapper object. It is recom ...

The element at index '0' is not defined for the data type 'number | [number, number]'

In my current project, I have a component named ComponentA which has a defined interface. Here is the snippet of the interface: interface A1 { isSingle: true; value: number; } interface A2 { isSingle: false; value: [number, number]; } exp ...

Retrieve the value of the Observable when it is true, or else display a message

In one of my templates, I have the following code snippet: <app-name val="{{ (observable$ | async)?.field > 0 || "No field" }}" The goal here is to retrieve the value of the property "field" from the Observable only if it is grea ...

Unable to exclude folder while creating production build is not functioning as intended

I've got a directory full of simulated data in the "src/api/mock" folder, complete with ts and JSON files. I'm attempting to have Webpack skip over them during the production build process. I attempted to implement the following rule, but unfortu ...

What Causes a Mongoose Query to Result in an Empty Array?

Hello, I have reviewed similar questions regarding the issue I am facing with developing an API. Despite trying different solutions, none seem to resolve my problem. When handling request and response payloads in my API, everything seems to be working fin ...

The specified reference token grant value of [object Object] could not be located in the store

Currently, I am working with NestJs along with the oidc passport strategy using identityserver. Below is a snippet of the code: import { UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; ...

Why does the data appear differently in Angular 9 compared to before?

In this particular scenario, the initial expression {{ bar }} remains static, whereas the subsequent expression {{ "" + bar }} undergoes updates: For example: two 1588950994873 The question arises: why does this differentiation exist? import { Com ...

Is it possible in Typescript to pass method signature with parameters as an argument to another method?

I am working on a React app where I have separated the actions into a different file from the service methods hoplite.actions.ts export const fetchBattleResult = createAsyncThunk<Result>( 'battle/fetchBattleResult', HopliteService.battleRe ...

Warning: Potential spacing issues when dynamically adjusting Material UI Grid using Typescript

When working with Typescript, I encountered an error related to spacing values: TS2322: Type 'number' is not assignable to type 'boolean | 7 | 2 | 10 | 1 | 3 | 4 | 5 | 6 | 8 | "auto" | 9 | 11 | 12'. No lint errors found Version: typesc ...

Battle of the Blobs: Exploring Blob Source in Google Apps Script

I've been exploring clasp, a tool that allows developers to work with Google Apps Script using TypeScript. Currently, I am working on a script that converts a Google Sheet into a PDF Blob and then uploads it to Google Drive. While the code is execut ...

What is the best way to reset a dropdown list value in angular?

Is there a way to erase the selected value from an Angular dropdown list using either an x button or a clear button? Thank you. Code <div fxFlex fxLayout="row" formGroupName="people"> <mat-form-field appearance=&quo ...

Preventing driver closure during test suites in Appium/Webdriverio: a step-by-step guide

Currently, I am in the process of testing a react native application with a specific test suite and test cases. The test case files I am working with are: login.ts doActionAfterLogin_A.ts Test Suite: [login.ts, doActionAfterLogin_A.ts] Issue at Hand: W ...