Типизированный интерфейс общего значения ключа в машинописном тексте

У меня есть следующий пример объекта:

let foo: Foo = {
  'key1': { default: 'foo', fn: (val:string) => val },
  'key2': { default: 42, fn: (val:number) => val },

  // this should throw an error, because type of default and fn don't match
  'key3': { default: true, fn: (val:string) => val }
}

Интерфейс должен выглядеть примерно так:

interface Foo {
  [key: string]: { default: T, fn: (val:T) => any }
}

Это, конечно, не работает, потому что T не определено.

Итак, я подумал об этом:

interface FooValue<T> {
  default: T;
  fn: (val:T) => any;
}

interface Foo {
  [key: string]: FooValue<?>
}

Но и там я застрял. Потому что я не могу определить общий тип FooValue.

Если я использую FooValue<any>, то, конечно, все набирается как any. Хотя это не работает.

Я хочу убедиться, что тип default и тип параметра fn всегда одинаковы.

Есть какое-нибудь решение? Или этого нельзя сделать?


person Benjamin M    schedule 29.03.2018    source источник
comment
как насчет interface Foo<T>?   -  person Explosion Pills    schedule 29.03.2018
comment
Затем я должен определить T при выполнении let foo: Foo<???> = {...}. Затем я бы установил общий тип для каждого ключа. Таким образом, все FooValue объекты должны быть одного типа. Но, как вы можете видеть в примере объекта: мне нужен другой общий тип для каждой пары "ключ-значение".   -  person Benjamin M    schedule 29.03.2018


Ответы (1)


Как насчет определения Foo<T> как сопоставленного типа, например это:

interface FooValue<T> {
  default: T;
  fn: (val: T) => any;
}

type Foo<T> = {
  [K in keyof T]: FooValue<T[K]>
}

В этом случае, если T - это какой-то нормальный тип объекта, например {a: string, b: number, c: boolean}, то Foo<T> - это Foo-ized его версия: {a: FooValue<string>, b: FooValue<number>, c: FooValue<boolean>}. Теперь вы можете создать вспомогательную функцию, которая принимает литерал объекта, только если он может быть выведен как Foo<T> для некоторого типа T:

function asFoo<T>(foo: Foo<T>): Foo<T> {
  return foo;
}

Эта функция работает, потому что компилятор TypeScript может делать вывод из сопоставленные типы, позволяющие выводить T из Foo<T>. Вот это работает:

let foo = asFoo({
  key1: { default: 'foo', fn: (val: string) => val },
  key2: { default: 42, fn: (val: number) => val }
});
// inferred as { key1: FooValue<string>; key2: FooValue<number>;}

А вот и не получается:

let badFoo = asFoo(
  key1: { default: 'foo', fn: (val: string) => val },
  key2: { default: 42, fn: (val: number) => val },
  key3: { default: true, fn: (val: string) => val }
}); 
// error! Types of property 'key3' are incompatible. 
// Type 'boolean' is not assignable to type 'string'.

Надеюсь, это поможет. Удачи!


Обновление: в приведенном выше коде предполагается, что вы согласны с выводом foo.key1.fn('abc') как типа any, поскольку FooValue<string>['fn'] определяется как функция, возвращающая any. Это как бы забывает тип вывода из исходного литерала объекта. Если вы хотите, чтобы foo запомнил тип возвращаемого значения fn методов его свойств, вы можете сделать эту немного другую вспомогательную функцию:

function asFoo<T, F>(foo: F & Foo<T>): F {
  return foo;
}

let foo = asFoo({
  key1: { default: 'foo', fn: (val: string) => val },
  key2: { default: 42, fn: (val: number) => val },
  // next line would cause error
  // key3: { default: true, fn: (val: string)=>val} 
})

const key1fnOut = foo.key1.fn('s') // known to be string
const key2fnOut = foo.key2.fn(123) // known to be number

И это работает. В этом случае asFoo() просто проверяет, является ли вход Foo<T> для некоторого T, но не приводит тип вывода к Foo<T>. В зависимости от ваших вариантов использования вы можете предпочесть это решение другому. И снова удачи.

person jcalz    schedule 29.03.2018
comment
Спасибо! Замечательная помощь! Думаю, мне нужна была вспомогательная функция. - person Benjamin M; 30.03.2018