首页 > 解决方案 > 结合接口来生成一个新的类型,并结合它们的属性

问题描述

我有一个Locale保存我的应用程序本地化的对象。

export const Locale = {
    EN: "en",
    DE: "de",
    EL: "el",
    FR: "fr",
}

export const Locales = Object.values(Locale)

我有另一个代表 API 类型的对象,即Trip

export interface Trip {
    id: number,
    title:  string,
    description: string,
    featured: boolean,
}

的某些字段是Trip可翻译的(在这种情况下为标题、描述),这意味着在某些情况下,API 会发送带有其语言环境的本地化字段。

例如:

{
  "id": 1,
  "title_el": "Greek Title",
  "title_de": "German title",
  "title_fr": "French Title",
  "title_en": "English Title,
  "description_en": "",
  "description_el": "",
  "description_de": "",
  "description_fr": "",
  "featured": true
}

我想要做的是以某种方式生成一种新类型LocalizedTrip,该类型具有可本地化的字段以及所有不可本地化的字段。

我尝试使用 Typescript 的 4.1 Key Remaps没有运气

我创建了一个代码框来启动派对: https ://codesandbox.io/s/xenodochial-frog-dr6de?file=/src/types.ts

export const Locale = {
  EN: "en",
  DE: "de",
  EL: "el",
  FR: "fr",
}

export const Locales = Object.values(Locale)


type LocalizedType<T> = {
  [K in keyof T as `[what here?]`]: () => T[K]
}

export interface Trip {
  id: number,
  title: string, //Maybe create another type for the localizable fields?
  description: string, 
  featured: boolean,
}


type LocalizedTrip = LocalizedType<Trip>

const test: LocalizedTrip = {

}

test.title_en
test.title_de

标签: typescripttypescript-generics

解决方案


I think you need to be clear about which keys should become localized. A default guess I have is those properties whose keys are not symbols and whose values are assignable to string:

type LocalizableProps<T> = Exclude<{
    [K in keyof T]-?: T[K] extends string ? K : never }[keyof T]
    , symbol>;

which produces this for Trip:

type TripStringProps = LocalizableProps<Trip>
// type TripStringProps = "title" | "description"

If that doesn't capture your intent, you can change LocalizableProps to some other type function which does (or gets close enough).


It also helps to have the type corresponding to the union of the locale strings. Right now Locale's properties are seen as just string, which is too wide for our purposes. In order not to lose track of the specific string literal values in your Locale object, we should use something like a const assertion:

export const Locale = {
    EN: "en", 
    DE: "de",
    EL: "el",
    FR: "fr",
} as const;

type Locales = typeof Locale[keyof typeof Locale]
/* type Locales = "en" | "de" | "el" | "fr" */

Now if you change your Locale object the compiler should notice and adapt automatically.


Finally we can define LocalizedType<T, K> where K defaults to LocalizableProps<T> but you can override it if necessary:

type LocalizedType<T, K extends Exclude<keyof T, symbol> = LocalizableProps<T>> = {
    [P in keyof T as  (
        P extends K ? `${P}_${Locales}` : P
    )]: T[P]
}

This is a fairly straightforward key remapping: for each key P in keyof T, if P is one of the specified keys K, use the union `${P}_${Locales}`, which becomes a union (e.g., if P is "x", then ${P}_{Locales} will be "x_en" | "x_de" | "x_el" | "x_fr". Otherwise, leave it unchanged.


Let's test it out:

type LocalizedTrip = LocalizedType<Trip>

/* type LocalizedTrip = {
    id: number;
    title_en: string;
    title_de: string;
    title_el: string;
    title_fr: string;
    description_en: string;
    description_de: string;
    description_el: string;
    description_fr: string;
    featured: boolean;
} */

Looks good, and let's see what happens if we specify a different key:

type LocalizedTripJustTitle = LocalizedType<Trip, "title">
/* type LocalizedTripJustTitle = {
    id: number;
    title_en: string;
    title_de: string;
    title_el: string;
    title_fr: string;
    description: string;
    featured: boolean;
} */

Playground link to code


推荐阅读