Как сделать так, чтобы аргумент строки проверки компилятором действовал в Typescript?

В TypeScript, как можно заставить компилятор определять, является ли строка допустимым аргументом для метода / функции?

Прямо сейчас я использую строковые литералы для достижения этой цели. Например,

type ValidLetter = "A" | "B" | "C" | "D"; // string literal definition

public PostLetter(letter: ValidLetter) {
    ...
    api.post("https://example.com/letters/", letter);
    // POST method only accepts "A", "B", "C", or "D"
}

PostLetter("A") // All good!
PostLetter("Z") // Compiler error

Единственное, во время компиляции я не знаю значений, которые я передам методу Post. Я мог бы получить любую строку,

let a = "A";
let foobar = "foobar";

PostLetter(a) // Compiler error
PostLetter(foobar) // Compiler error

То, что я ищу, это способ проверки, является ли строка действительным членом строкового литерала. Я уже пытался использовать typeof, instanceof, определяемые пользователем охранники типов и кастинг. Кажется, ни у кого из них нет того, что нужно.

Как бы я определил это a является членом ValidLetter а также foobar не является? Или, возможно, строковые литералы - не тот путь.

2 ответа

Решение

Вы должны быть в состоянии сделать это со смесью карт значений, а также с определенными пользователем типами защиты:

const ValidLetterMap = { A: 1, B: 1, C: 1, D: 1 };
type ValidLetter = keyof typeof ValidLetterMap;

declare function postLetter(letter: ValidLetter): void;

postLetter("A"); // ok
postLetter("E"); // err

const a = "A";
postLetter(a); // ok

let a$ = "A";
postLetter(a$); // err, a$ is of type string since it is mutable

function isValidLetter(letter: string): letter is ValidLetter {
    return letter in ValidLetterMap;
}

if (isValidLetter(a$)) {
    postLetter(a$); // now ok because we've "proven" that a$ is a valid letter
}

Изменить: вот общая форма, опираясь на небольшой взлом, чтобы выставить набор текста.

class StringLiteral<T extends string> {
    private literalSet: {[P in T]: true};

    // sort of a hack so we can expose a union type of valid letters
    public get typeProvider(): T {
        throw new Error("typeProvider is only meant for typing info, it has no value");
    }

    constructor(...literals: T[]) {
        this.literalSet = literals.reduce(
            (acc, curr) => (acc[curr] = true, acc),
            {} as {[P in T]: true}
        );
    }

    public isValid(candidate: string): candidate is T {
        return candidate in this.literalSet;
    }
}

// how to use
const lettersLiteral = new StringLiteral("A", "B", "C", "D");

declare function postLetter(letter: typeof lettersLiteral.typeProvider): void;

let a$ = "A";
postLetter(a$); // not ok

if (lettersLiteral.isValid(a$)) {
    postLetter(a$); // ok!!
}

TypeScript просто не выполняет никакой проверки типов во время выполнения. Проверка типов происходит во время компиляции, а информация о типах не включается в создаваемый файл JavaScript.

post.ts

type ValidLetter = "A" | "B" | "C";

function post(letter: ValidLetter) {
}

Создает следующий JavaScript:

post.js

function post(letter) {
}

Таким образом, вы должны заново указать тип проверки вручную в коде времени выполнения:

type ValidLetter = "A" | "B" | "C";

function post(letter: ValidLetter) {
  if (letter !== "A" && letter !== "B" && letter !== "C") throw "error!";
}

Не так уж плохо. Но это немного избыточно, не так ли?

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

import { Literal, Union, Static } from 'runtypes'

const ValidLetter = Union(Literal('A'), Literal('B'), Literal('C'));
type ValidLetter = Static<typeof ValidLetter>;

function post(letter: ValidLetter) {
    ValidLetter.check(letter);
}

Так что теперь вы получаете как полные проверки во время компиляции, так и проверки во время выполнения.

Другие вопросы по тегам