Как создать экземпляр различного объединения в TypeScript на основе предоставленного типа? - PullRequest
0 голосов
/ 07 ноября 2019

Учитывая различенное объединение MyUnion, я хочу вызвать функцию createMyUnionObject с одним из type s из MyUnion и значением для value, которое должно быть правильного типа.

type MyUnion =
    {
        type: 'one'
        value: string
    } |
    {
        type: 'two',
        value: number
    } |
    {
        type: 'three',
        value: boolean
    };

function createMyUnionObject<
    T extends MyUnion['type'],
    MU extends Extract<MyUnion, { type: T }>,
    V extends MU['value']
>(type: T, value: V): MU {
    // Real code is conditional and may modify `value`, this is a simple example with the same problem I'm having
    return { type, value };
}

Если вы попытаетесь вызвать createMyUnionObject, вы увидите, что TypeScript правильно выводит значения вне определения функции. То есть createMyUnionObject('one', false) будет иметь нужную ошибку компиляции Argument of type 'false' is not assignable to parameter of type 'string'.

В определении функции, однако, я вижу Type '{ type: T; value: V; }' is not assignable to type 'MU'. и не понимаю почему - как я могу построить объекттак что TypeScript убедился, что это правильный тип?

1 Ответ

2 голосов
/ 07 ноября 2019

В настоящее время TypeScript не может сузить типы параметров универсального типа с помощью анализа потока управления. В идеале я бы хотел написать вашу функцию следующим образом:

function createMyUnionObject<T extends MyUnion["type"]>(
    type: T,
    value: Extract<MyUnion, { type: T }>["value"]
): Extract<MyUnion, { type: T }> {
    return { type, value }; // error ?
}

, но мы не можем (обратите внимание, что нам нужен только один универсальный параметр ... из T вы должны иметь возможностьдля вычисления value и возвращаемых типов). Даже если для любого конкретного значения type и его соответствующего value компилятор может проверить, что {type, value} присваивается MyUnion, он не может сделать это с обобщениями.


Одно препятствие состоит в том, что компилятор подозревает, что ваш type может иметь полный тип объединения "one" | "two" | "three", и, конечно, value может поэтому быть string | number | boolean, а затем {type, value}может не быть назначенным на MyUnion:

const t = (["one", "two", "three"] as const)[Math.floor(Math.random() * 3)];
const v = (["one", 2, true] as const)[Math.floor(Math.random() * 3)];
createMyUnionObject(t, v); // uh oh

Вы хотите сказать компилятору: нет, T может быть только либо "one", или "two", или "three", но не объединение двух или более из них. Это еще не часть языка, но было запрошено 1035 *. Если это когда-либо будет реализовано, его все равно нужно будет объединить с неким анализом потока управления, который сделал несколько проходов по телу, делая каждое возможное сужение по очереди. Я предложил это связанные вещи ), но опять же, это еще не часть языка.


Пока что я бы сказал, чтоесли вы хотите, чтобы ваш код компилировался и двигался дальше, вам нужно будет использовать утверждение типа , чтобы просто сказать компилятору, что вы знаете, что вы делаете безопасно (и что вы просто не собираетесьбеспокойтесь о ком-то, кто делает сумасшедшие Math.random() вещи, подобные приведенному выше коду):

function createMyUnionObject<T extends MyUnion["type"]>(
    type: T,
    value: Extract<MyUnion, { type: T }>["value"]
) {
    return { type, value } as any as Extract<MyUnion, { type: T }>; // assert
}

Это должно сработать, и вы можете использовать createMyUnionObject(), как хотите. Хорошо, надеюсь, это поможет. Удачи!

Ссылка на код

...