Некоторое время я пытался написать правильные типы для функции сопоставления / сворачивания. Надеюсь, кто-нибудь поможет мне понять, что я делаю неправильно.

Функция должна быть универсальной, но учтите, что в качестве примера у вас есть следующий тип объединения.

class Square {
  type = "Square" as const
  constructor(public side: number) {}
}
class Circle {
  type = "Circle" as const
  constructor(public radius: number) {}
}
class Rectangle {
  type = "Rectangle" as const
  constructor(public width: number, public height: number) {}
}

type Shape = Square | Circle | Rectangle

Мне нужна функция сопоставления, чтобы я мог обрабатывать все параметры и иметь для этого безопасность типов. Это вроде работает с этим решением.

type XMap<T, Key extends keyof T> = { [K in T[Key]]: T extends { type: K } ? T : never }

type XPattern<T, Key extends keyof T, R> = { [K in keyof XMap<T, Key>]: (shape: XMap<T, Key>[K]) => R }

function matcher<T, Key extends keyof T, R>(key: Key, pattern: XPattern<T, Key, R>): (shape: T) => R {
  return shape => pattern[shape[key]](shape as any)
}

const area = matcher<Shape, 'type', number>('type', {
  Square: square => square.side * square.side,
  Circle: circle => circle.radius * circle.radius * Math.PI,
  Rectangle: rect => rect.height * rect.width
})

const shapes = [new Circle(4.0), new Square(5.0), new Rectangle(6.0, 7.0)]

console.log(`Areas: ${shapes.map(area)}`)

Playground Link

Как вы можете видеть на этой игровой площадке, он все еще выдает ошибки, но каким-то образом показывает мне правильные типы в сопоставлении. Не знаю, как это исправить.

Спасибо за ваше время и помощь.

0
ian 21 Фев 2021 в 22:10

1 ответ

Лучший ответ

Вот один из способов заставить его работать. Обратите внимание, что для сопоставления мне пришлось использовать разные типы:

type Shape = Square | Circle | Rectangle

/**
 * Given a type, a key on that type, and a potential value,
 * return if type is an object that extends { [key]: value }
 * 
 * Since union types distribute over conditionals (it tries one at a time),
 * this lets us start out with Shape and end up with a single of the three
 */
type XMatching<T, TKey extends keyof T, TValue> = T extends { [P in TKey]: TValue } 
  ? T 
  : never

type Test01 = XMatching<Shape, 'type', 'Circle'> // Circle
type Test02 = XMatching<Shape, 'type', 'Square' | 'Circle'> // Square | Circle
type Test03 = XMatching<Shape, 'type2', 'Square'> // ERR since `type2` is not a shared field
type Test04 = XMatching<Shape, 'type', 'Ellipse'> // never

/**
 * Given a type T, a key on that type TK, and another arbitrary type TR,
 * if type.key extends string (and therefore can be used as a key itself)
 * generate a map from all possible values of type.key to 
 * functions that take in the particular object with that particular value of type.key, e.g.
 * 
 * 'Circle' -> Circle
 * 'Square' -> Square
 * 
 * and returns the arbitrary type TR.
 * 
 * Otherwise, return never.
 * 
 * (there are certainly ways to write this without the conditional)
 */
type XPattern<T, TKey extends keyof T, TR> = T[TKey] extends string
  ? { [P in T[TKey]]: (matched: XMatching<T, TKey, P>) => TR }
  : never

function matcher<T, TKey extends keyof T, TR>(key: TKey, pattern: XPattern<T, TKey, TR>): (matched: T) => TR {
  return shape => pattern[shape[key]](shape)
}

const shapes = [new Circle(4.0), new Square(5.0), new Rectangle(6.0, 7.0)]

const area = matcher<Shape, 'type', number>('type', {
  Square: square => square.side * square.side,
  Circle: circle => circle.radius * circle.radius * Math.PI,
  Rectangle: rect => rect.height * rect.width
}) // OK!

const badArea01 = matcher<Shape, 'type', number>('type', {
  Square: square => square.side * square.side,
  Circle: circle => circle.radius * circle.radius * Math.PI,
  Rectangle: rect => rect.height * rect.width,
  Ellipse: ellipse => ellipse.radiusA * ellipse.radiusB * Math.PI 
}) // ERR since Ellipse has no match

const badArea02 = matcher<Shape, 'type', number>('type', {
  Square: square => square.side * square.side,
  Circle: circle => circle.radius * circle.radius * Math.PI,
  Rectangle: rect => "real big!" 
}) // ERR since returns a string, not a number
2
y2bd 21 Фев 2021 в 20:19