0

The EventBridgeEvent interface found in "@types/aws-lambda" is defined as:

export interface EventBridgeEvent<TDetailType extends string, TDetail>

Notice that TDetailType extends the string type. What's interesting to me is that I can define a variable as:

event: EventBridgeEvent<'stringLiteralFoo', Bar>

In order to prevent copying/pasting the string literal I tried to define it as a variable but then I am no longer able to use it as a type.

const stringLiteralFooVar = 'stringLiteralFoo'
event: EventBridgeEvent<stringLiteralFooVar, Bar>

The error I get is:

'stringLiteralFoo' refers to a value, but is being used as a type here. Did you mean 'typeof stringLiteralFoo'?

The full definition of the Event Bridge Event is

export interface EventBridgeEvent<TDetailType extends string, TDetail> {
  id: string;
  version: string;
  account: string;
  time: string;
  region: string;
  resources: string[];
  source: string;
  'detail-type': TDetailType;
  detail: TDetail;
  'replay-name'?: string;
}

So to find the detail-type of a specific event I'm doing the following:

event: EventBridgeEvent<'stringLiteralFoo', Bar>
if (event['detail-type'] == 'stringLiteralFoo') {
    // logic here
}

But I'd like to prevent copying/pasting the 'stringLiteralFoo' literal.

Here is the minimal reproducible example:

export interface EventBridgeEvent<TDetailType extends string, TDetail> {
  'detail-type': TDetailType;
  detail: TDetail;
}

const event: EventBridgeEvent<'Foo1' | 'Foo2', Bar> = {
  'detail-type': 'Foo1',
  detail: {}, // of type Bar
}
if (event['detail-type'] == 'Foo1') {
  // logic here
}

interface Bar {}

So my final question is, in the example above, how can I prevent copying and pasting the literal string; 'stringLiteralFoo'?

Archmede
  • 1,592
  • 2
  • 20
  • 37
  • 1
    Types and runtime variables are completely separate. If you would like to get the type of a variable, you may use `typeof myVariable`. In this case, `typeof stringLiteralFooVar`, which would give you the type `"stringLiteralFooVar"`. String literal types are *very* different from string literals. – kelsny Sep 09 '22 at 19:11
  • @kelly isn't `typeof stringLiteralFooVar` == 'string' – Archmede Sep 09 '22 at 19:20
  • 3
    @Archmede https://www.typescriptlang.org/docs/handbook/2/typeof-types.html – Tobias S. Sep 09 '22 at 19:22
  • 2
    If you use `typeof xxx` in a type context, that's the [TS `typeof` *type operator*](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html) and not the [JS `typeof` operator](//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof). Types and values may have the same names but they live in different worlds (types are completely erased when TS compiles to JS, but values do appear at runtime). You should probably read [the TS handbook docs on literal types](//www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) and probably the whole handbook. – jcalz Sep 09 '22 at 19:24
  • Could you be clear about the specific question here? Is it really "why is a string literal a valid type" as the title suggests? If so then the docs on literal types is the canonical answer, and maybe in the implementing PR in [ms/TS#5185](//github.com/microsoft/TypeScript/pull/5185). Is it "why can't I use a string variable as a type"? If so then the answer is "types and values are distinct" along with some variant of the long explanation in [this answer](//stackoverflow.com/a/50396312/2887218). Is it about "I'd like to prevent copying/pasting"? If so then we need a [mre] of that code. – jcalz Sep 09 '22 at 19:29
  • In any case, it would be nice to see, in the question text itself, a single well-defined question, the answer to which would constitute an answer to the whole thing. Let us know. – jcalz Sep 09 '22 at 19:30
  • 1
    @jcalz I updated the question to have the question I was answered, thanks – Archmede Sep 09 '22 at 19:33
  • The title should reflect your actual question, which is apparently about preventing copying and pasting. And much of the question text is no longer related to the question; consider removing it, maybe? Given the code example, the answer is not to check `event['detail-type']` at all because it is definitely going to be `'stringLiteralFoo'`. There might be a more palatable answer but I would like to see a [mre] that can be pasted, as-is, into a [standalone IDE](//www.typescriptlang.org/play) to demonstrate the issue. Right now I don't know what `Bar` is, etc., so it's hard to answer concretely. – jcalz Sep 09 '22 at 19:40
  • @jcalz I reworded the question and added an example which compiles in an IDE, lmk if you need anything else. Thank you – Archmede Sep 09 '22 at 19:53
  • Thanks! I'm still confused though about why you're doing the check; it will always return `true` there. Could you make the example a little more motivated so I can suggest a change that does the same thing with less writing? But in any case you could do [this](https://tsplay.dev/w1pvKW); does that work for your needs? Or do you want something else? – jcalz Sep 09 '22 at 19:58
  • @jcalz I updated the example. Basically what I want to try preventing is `type of var1 | typeof var2 ...` – Archmede Sep 09 '22 at 20:01
  • So like [this](https://tsplay.dev/WKRDzm) maybe? – jcalz Sep 09 '22 at 20:03
  • yeah that's better but isn't that a bit convoluted? Can you give a brief description of why that works? – Archmede Sep 09 '22 at 20:06
  • Do you want that description in an answer? Or in a comment before you know if you'll accept the answer? (And please mention me with @jcalz if you reply or I will not be notified). – jcalz Sep 09 '22 at 20:16
  • @jcalz yeah I'd like it as answer, then I can accept it as well since you've answered my question – Archmede Sep 09 '22 at 20:24
  • 1
    Okay great, I'll write it up when I get a chance, along with an explanation. Might be a few hours before I get to it. – jcalz Sep 09 '22 at 20:26

1 Answers1

2

If you want stop yourself from having to mention the same quoted strings multiple times both as string literal values and string literal types, you could do something like this. First, create a const-asserted array of the relevant string literals:

const detailTypes = ["Foo1", "Foo2"] as const;

That const assertion is telling the compiler that you don't want it to simply infer the type of detailTypes as string[], a mutable and unordered array of an arbitrary number of unknown strings. Instead, you want it to infer an ordered, fixed-length, readonly tuple type consisting of the string literal types of the contents. So the compiler infers this for detailTypes:

// const detailTypes: readonly ["Foo1", "Foo2"]

Armed with this, we can use various value and type operators to get the types you care about without having to write out the quoted string values and types again. For example:

const event: EventBridgeEvent<typeof detailTypes[number], Bar> = {
    'detail-type': detailTypes[0],
    detail: {}, // of type Bar
}

if (event['detail-type'] == detailTypes[0]) {
}

The type typeof detailTypes uses the TypeScript typeof type query operator to resolve to readonly ["Foo1", "Foo2"]. In order to get the union type "Foo1" |"Foo2" from this, we can index into it with the number index: typeof detailTypes[number]. You can think of this as: what type do you get if you index into detailTypes with a valid key of type number? The answer is "either "Foo1" or "Foo2"", and hence the union.

And you can write detailTypes[0] or detailTypes[1] to grab the individual element values. If you need their types, you can use the typeof type operator again, either directly

type ElemOne = typeof detailTypes[1];
// type ElemOne = "Foo2"

or indirectly

const elemOne = detailTypes[1];
type ElemOneAlso = typeof elemOne;
// type ElemOneAlso = "Foo2"

So there you go.


Note that in type ElemOne = typeof detailTypes[1], the 1 is a numeric literal type and the [1] is an indexed access type, whereas in const elemOne = detailTypes[1], the 1 is a numeric literal value, and the [1] is an actual indexing operation in JavaScript. They look the same, and in some sense they talk about comparable operations, but they are not the same. Explaining the difference can be tricky. The type ElemOne = ... line is purely acting at the type level, with TypeScript type operators, while the const elemOne = ... line is acting at the value level, with JavaScript operators. The type system is completely erased from the emitted JavaScript. See this answer to a different question about how types and values exist in separate levels and how they might look the same but sometimes refer to very different things.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360