类型标记,用于提示以其他方式推断的文字对象类型

时间:2019-04-01 15:01:10

标签: typescript

我正在创建一个函数,该函数接受config(结构非常可变的文字对象),然后根据配置类型输出同样可变的结构。我想尽可能地使用TypeScript推断来检测配置的类型,但是我还需要该函数的用户能够提示的某些部分。

这可以满足我的需求

type Type<T> = (_: T) => T;
const type =<T>(): Type<T> => T => T;

type ListConfig<T> = {
    itemType: Type<T>;
}

type Config = {
    [name: string]: ListConfig<any>; 
}

type ItemType<L> = L extends ListConfig<infer T> ? T : never;

const createLists = <C extends Config>(
    cfg: C
): { [K in keyof C]: Array<ItemType<C[K]>> } => {
    return {} as any;
}

const config = {
    numbers: {
        itemType: type<number>(),
    },
    strings: {
        itemType: type<string>(),
    }
};

// This is correctly inferred as { numbers: number[], strings: string[] }
const lists = createLists(config);

Playground

请注意itemType,它们的存在只是为了暗示createLists的返回类型。上面的方法有效,但也让人觉得很棘手-我正在创建函数只是为了让TypeScript了解所需的类型。

最干净的解决方案是什么?

我一直希望按照以下方式寻求解决方案

type ListConfig<T> = {
}

type Config = {
    [name: string]: ListConfig<any>; 
}

type ItemType<L> = L extends ListConfig<infer T> ? T : never;

const createLists = <C extends Config>(
    cfg: C
): { [K in keyof C]: Array<ItemType<C[K]>> } => {
    return {} as any;
}

const listOf = <T, C extends ListConfig<T>>(c: C): C => c;

const config = {
    numbers: listOf<number>({ // <-- error here
        somOtherPartOfTheListConfig: "foo",
    }),
    strings: listOf<string>({}), // <-- error here
};

// This is correctly inferred as { numbers: number[], strings: string[] }
const lists = createLists(config);

Playground

但是,我仍然希望通过打字稿(项目类型除外)推断出config[K]的类型,因此,为了实现这一点,我需要partial type argument inferrence

编辑:我正在做的是一个简化Redux存储/动作创建的库。所以基本上我想提供State<T>createRedux允许我这样做:

const myConfig = {
  $combine: {
    listOfNumbers: {
      $preset: 'list' as 'list',
      $item: {
        $type: {
          $itemType: type<string>(),
        }
      },
    }
  }
};

type MyState = State<typeof myConfig>;
// MyState == { listOfNumbers: string[] }
const { Actions } = createRedux(myConfig);
// typeof Actions == {
//   listOfNumbers: {
//     push: (item: string) => ReduxAction
//   }
// }

1 个答案:

答案 0 :(得分:1)

我不知道您真正想如何在这里进行,因为从根本上讲,在运行时没有任何东西在乎您使用的类型,对吗?如果您有初始数组内容,或者是type guard function或其他一些运行时工件,那么我会提出一些具体建议。


一种可能的前进方式是表示类型名称到类型的映射,并要求配置使用以下名称:

interface TypeNames {
  string: string,
  number: number,
  boolean: boolean,
  arrayOfStrings: string[],
  // etc
}

type ListConfig<K extends keyof TypeNames> = {
  itemType: K
}

type Config = {
  [name: string]: ListConfig<keyof TypeNames>;
}

const createLists = <C extends Config>(
  cfg: C
): { [K in keyof C]: Array<TypeNames[C[K]['itemType']]> } => {
  return {} as any;
}

// This is correctly inferred as { numbers: number[], strings: string[] }
const lists = createLists({
  numbers: { itemType: "number", otherStuff: "foo" },
  strings: { itemType: "string", otherStuff: 1234 }
});

这可以尽其所能,但是需要一个固定的(可能是可扩展的)类型列表。


另一种方法是要求调用者createLists()为type参数指定对象类型,其键与配置键相匹配,并且值是相关的列表类型:

const createLists = <T>() => <C extends Record<keyof T, any>>(
  cfg: C
): { [K in keyof C]: K extends keyof T ? Array<T[K]> : any } => {
  return {} as any;
}

// This is correctly inferred as { numbers: number[], strings: string[] }
const lists = createLists<{ numbers: number, strings: string }>()({
  numbers: { otherStuff: "foo" },
  strings: { otherStuff: 1234 }  
});

(请注意,curried函数在没有部分类型推断的情况下。)这有点 ,因为您需要指定numbers和{{1} }键两次,但对我来说却干净很多。


无论如何,希望那些能给您一些想法。祝你好运!