Как сделать так, чтобы аргумент строки проверки компилятором действовал в 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);
}
Так что теперь вы получаете как полные проверки во время компиляции, так и проверки во время выполнения.