1

I am trying to create a class or constructor function, which I specify a list of values to be used in the type of one of it's methods.

As an example I have this code.

const MyAnimal = function (animal: string[]) {
  type Animal = typeof animal[number]

  this.getAnimal = (url: Animal) => {
    console.log(url)
  }
}

const animalTest = new MyAnimal(['sheep', 'dog', 'cat'])
// I would like this to fail as 'mouse' is not part of the array ['sheep', 'dog', 'cat']
animalTest.getAnimal('mouse')

I want getAnimal to have the type 'sheep' | 'dog' | 'cat' and for the intellisense to warn me if I add something different

Is this possible?

0

2 Answers 2

3

You can do that via a generic type parameter and a readonly array of animals, like this;

class MyAnimal<Animal> {
    constructor(public animals: readonly Animal[]) {
    }
    getAnimal(url: Animal) {
        console.log(url);
    }
}

const animalTest = new MyAnimal(['sheep', 'dog', 'cat'] as const);
animalTest.getAnimal('mouse'); // Error as desired
animalTest.getAnimal('sheep'); // Works

Playground link

TypeScript can infer the string literal union type to provide as the type argument for the Animal type parameter because the array is readonly (which we satisfy when calling the constructor via as const), so TypeScript knows it won't change at runtime.

Sign up to request clarification or add additional context in comments.

5 Comments

Amazing thank you so much for your reply.. as a next step - is there a way to instead of passing in an array of strings, to pass in an array of objects, where the objects contain a string you want to infer as the type, e.g. { name: 'tiger', url: 'https:....' }, { name: 'sheep', url: 'https://...' }
@user3284707 - Sure, TypeScript can infer that, too, you just specify the shape of the objects in terms of the Animal type parameter: tsplay.dev/mqQZ2m
This is almost exactly what I need, other than I need to try and now pass in an array of items rather than having them hard coded like in your example. Is this possible, when I have tried it's come back as unknown
I have tried here but it's not working any ideas? typescriptlang.org/play?#code/…
@user3284707 - You can't do this with dynamic data, because types are a compile-time concept in TypeScript. See the note at the end of the answer.
1

In order to infer literal type, you can use variadic tuple types:

type Animal<Name extends string> = {
    name: Name,
    url: `https://${string}`
}

class MyAnimal<
    Name extends string,
    Item extends Animal<Name>,
    List extends Item[]
    > {
    constructor(public animals: [...List]) {
    }

    // You should get "url" from infered list and not from Item["url"]
    getAnimal<Url extends List[number]['url']>(url: Url) {
        console.log(url);
    }
}

const animalTest = new MyAnimal(
    [
        { name: 'sheep', url: 'https://sheep.com', },
        { name: 'dog', url: 'https://dog.com', },
        { name: 'cat', url: 'https://cat.com', }
    ]);

animalTest.getAnimal('https://dog.com'); // ok
animalTest.getAnimal('https://mouse.com'); // expected error

Playground

If you want to learn more about literal type inference, you can check my article. The goal is to provide as much as possible constraints to generic parameter. If there are enough constraints TS will infer literal type.

1 Comment

AMAZING Thank you so much

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.