How to require a specific string in TypeScript interface
Asked Answered
M

10

318

I'm creating a TypeScript definition file for a 3rd party js library. One of the methods allows for an options object, and one of the properties of the options object accepts a string from the list: "collapse", "expand", "end-expand", and "none".

I have an interface for the options object:

interface IOptions {
  indent_size?: number;
  indent_char?: string;
  brace_style?: // "collapse" | "expand" | "end-expand" | "none"
}

Can the interface enforce this, so if you include an IOptions object with the brace_style property, it will only allow a string that is in the acceptable list?

Muldoon answered 11/11, 2014 at 0:18 Comment(2)
Please revisit the answers to this questionLeucocytosis
Kinda along the lines of the comment from @Leucocytosis , why do you prefer RyanQ's answer over Denis Khay's? Denis' seems more broadly applicable imo.Raneeraney
T
452

This was released in version 1.8 as "string literal types"

What's New in Typescript - String Literal Types

Example from the page:

interface AnimationOptions {
  deltaX: number;
  deltaY: number;
  easing: "ease-in" | "ease-out" | "ease-in-out";
}
Torn answered 10/2, 2016 at 10:16 Comment(2)
It requires copying and pasting line with strings wherever in app you would need to pass string of this type as an argument. If you are always using whole AnimationOptions that solution seems ok, but for use cases when you may only pass 'easing' type it is wrong. See Denis Khay answer, it is far better.Fanatic
TypeScript Docs: Literal TypesDecurrent
E
355

Try this

export type ReadingTypes = 'some'|'variants'|'of'|'strings';

export interface IReadings {
   param:ReadingTypes
}

Edit: Many thanks for upvotes, but, as time passed and me evolved as a developer :), now in most of the cases I wouldn't recommend this approach anymore. Yes it still valid but the point is the construction above is very similar to enum structure so why not use enum instead (advantages below):

export enum ReadingTypes {
    Some = 'some',
    Variants = 'variants',
    Of = 'of',
    Strings = 'strings',
}
export interface IReadings {
   param: ReadingTypes
}

Advantages: (Yes, might be it is more like IMHO, I understand, but, nonetheless)

  1. It is more readable when you see it in the code, for example
if(item.reading === 'some') {
...
}
// vs 
if(item.reading === ReadingTypes.Some) {
...
}

In first case when you read the code you could not catch, from the first glance, that .reading field can only contain few certain params, and not like, any string value.

  1. When you write the code you will have better assistance of your editor if you use enums - it is enough to remember name of enum and write it and it will show you all variants of enum. Yeah, with the first type ('some' | 'variants' ... ) it can do so too, but it does it less.. um.. eagerly
Exercise answered 21/8, 2017 at 15:51 Comment(6)
This should be marked as correct, because it allows reusing created type anywhere in an application.Fanatic
This is exactly what I was looking for as a way to pass an array of strings as values to an enumerated type. By skipping over enum completely and using type, I was able to pass the following example: const myVar: ReadingTypes = ["some"|"variants"]; while still maintaining a dynamic enum typing syntax. Completely agreed that this should have been the answer marked as correct.Buna
is there a way to use this but softly typed like this type VisibilityTypes = VISIBILITY.PUBLISH | VISBIBILITY.DRAFT I get a namespace not found error, however when I hard code them it seems to workSubheading
I found a way, and added a new answerSubheading
i tried first to equal the type to say 'json' | 'text' and it did not work but when i set it to be say ResponseType and then did type ResponseType = 'json' | 'text' it did work, thank you!Berryberryhill
This is not working for me. In my case I want to restrict various properties values of an object, but I can only use "param:" once. :/ How can I add more restricted string properties to my object? There's not much about the 'param' reserved word in TS.Logan
L
26

TS offers a typing to specific string values, which are called String literal types.

Here is an example of how to use them:

type style =  "collapse" | "expand" | "end-expand" | "none";

interface IOptions {
  indent_size?: number;
  indent_char?: string;
  brace_style1?:  "collapse" | "expand" | "end-expand" | "none";
  brace_style2?:  style;
}

// Ok
let obj1: IOptions = {brace_style1: 'collapse'};

// Compile time error:
// Type '"collapsessss"' is not assignable to type '"collapse" | "expand" | "end-expand" | "none" | undefined'.
let obj2: IOptions = {brace_style1: 'collapsessss'};
Linalool answered 8/8, 2019 at 12:47 Comment(0)
S
15

In TypeScript 2.4 onward you can use String Enums

I favour this approach because it avoids the need to have the same hard coded string in more than one place.

Its possible to make an enum where the values are strings

export enum VISIBILITY {
  PUBLISH = "publish",
  DRAFT = "draft"
}

This enum can then be used as a type on an interface or class

export interface UserOptions  {
  visibility:  VISIBILITY 
}
Subheading answered 16/7, 2020 at 15:51 Comment(1)
BTW all caps reduces readability accessibility.huit.harvard.edu/design-readabilitySuneya
L
10

Maybe not exactly what you wanted, but Enums seem like a perfect solution for you.

enum BraceStyle {Collapse, Expand, EndExpand, None}

interface IOptions {
  indent_size?: number;
  indent_char?: string;
  brace_style?: BraceStyle
}

Enums are, however, number-based. It means that during runtime a real value for e.g. BraceStyle.Collapse will be 0 in this case. But you can use them with other, even non-typescript scripts, since they compile to objects. This is how BraceStyle will look after compile&run:

{
    0: "Collapse",
    1: "Expand",
    2: "EndExpand",
    3: "None",
    Collapse: 0,
    Expand: 1,
    EndExpand: 2,
    None: 3
}

If you want strings instead, you can use a class with static members, as described here

Lamina answered 11/11, 2014 at 0:30 Comment(1)
enums can have string values (now at least, not sure if they could in 2014). enum BraceStyle { Collapse = "Collapse", Expand = "Expand" }.Stroup
T
2
function keysOf<T>(obj: T, key: keyof T) { return obj[key]; }
interface SomeInterface {
   a: string;
}
const instance: SomeInterface = { a: 'some value'};
let value = keysOf<SomeInterface>(instance, 'b'); // invalid
value =  keysOf<SomeInterface>(instance, 'a'); // valid
Trust answered 12/7, 2019 at 2:20 Comment(0)
K
1

enum is probably the best solution, but if you have your values as keys of a const object and you can't change that, the syntax would be

brace_style?: typeof BRACE_STYLES[keyof typeof BRACE_STYLES];

where BRACE_STYLES is the name of the const object

Katt answered 27/9, 2021 at 22:18 Comment(0)
S
1

Most likely you want the enum options. But if you do not, and you really need your const to exist, the keyOf that some answers post here works, with a slight modification:

export const BRACE_STYLES = {
  collapse: 'collapse',
  'end-expand': 'end-expand',
  expand: 'expand',
  none: 'none'
}

export type BraceStyle = keyof typeof BRACE_STYLES

export interface IOptions {
  indent_size?: number
  indent_char?: string
  brace_style?: BraceStyle
}

This will actually get you the "collapse" | "expand" | "end-expand" | "none" effect you want, still allow the const to exists, without the need of hardcoding the type as well.

Simba answered 1/8, 2022 at 1:59 Comment(0)
C
0

You can create a custom type as stated by others.
I would add to that, that you can also infer that created type from an object const:

export const braceStyles = {
  collapse: "collapse",
  expand: "expand",
  end-expand: "end-expand",
  none: "none"
}

export type braceStyle = typeof braceStyles[keyof typeof braceStyles]

export interface IOptions {
  indent_size?: number;
  indent_char?: string;
  brace_style?: bracestyle;
}

That way you don't have to use enum and you can also use the object properties everywhere you need them, where they will be of type string, not type enum.member

objects vs enums

Chrisom answered 28/2, 2022 at 10:23 Comment(0)
C
0

This answer is a bit broad but can be extended to many use cases. You would make use of MappedTypes.

Here's an use case:


// Let's say that you have different string literals defining a 'type' property
// i.e: 'text' | 'video' | 'image' […]
// You would like each 'type' to 'enable' or 'disable' different   
// properties based on the `type` property:
const fooObj: MyObject = {
    type: "foo",
    value: {
        sharedProp: "shared",
        fooProperty1: 123,
        fooProperty2: "hello"
    }
};

const barObj: MyObject = {
    type: "bar",
    value: {
        sharedProp: "shared",
        barProperty1: true,
        barProperty2: new Date()
    }
};



type CommonProps = {
    sharedProp: "shared";
};

type FooProperties = {
    fooProperty1: number;
    fooProperty2: string;
    // ... other foo properties
};

type BarProperties = {
    barProperty1: boolean;
    barProperty2: Date;
    // ... other bar properties
};

type AllProperties = {
    foo: FooProperties,
    bar: BarProperties,
    // ... other discriminators and their properties
};

type MyObject = {
    [K in keyof AllProperties]: {
        type: K;
        value: AllProperties[K] & CommonProps;
    };
}[keyof AllProperties];


Chrysa answered 7/8, 2023 at 12:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.