Dart - How to create custom types on the fly like in Typescript?

6k views Asked by At

I want to create a custom type in Dart like I would do in typescript. This type should be a subtype of String, accepting only some values.

For example, in Typescript I would do:

type myType = 'HELLO' | 'WORLD' | '!'

How can I do the same stuff in Dart?

3

There are 3 answers

0
Michael Horn On BEST ANSWER

Original Answer (see updates below): This isn't possible at the language level in Dart - There are a couple alternatives though.

You could simply define an enum along with a method to derive a string from your enum:

enum MyType {
  hello,
  world,
  exclamationPoint,
}

String myTypeToString(MyType value) {
  switch (value) {
    case MyType.hello: 
      return 'HELLO';
    case MyType.world: 
      return 'WORLD';
    case MyType.exclamationPoint: 
      return '!';
  }
}

Update 2: In Dart 3 we have yet another way, using sealed classes:

sealed class MyType {
  @override
  String toString() {
    return switch (this) {
        Hello() => "Hello",
        World() => "world",
        Exclamation() => "!"
    };
  }
}

class Hello extends MyType {}
class World extends MyType {}
class Exclamation extends MyType {}

https://dart.dev/language/class-modifiers#sealed


Old Update: Now that Dart 2.17 supports declaring methods on enums, it's possible to do this a bit more cleanly than in my original answer:

enum MyType {
  hello,
  world,
  exclamationPoint;

  @override
  String toString() {
    switch (this) {
      case MyType.hello: 
        return 'HELLO';
      case MyType.world: 
        return 'WORLD';
      case MyType.exclamationPoint: 
        return '!';
    }
  }
}

Or you could define a class with three named constructors, and override the toString method:

class MyType {
  final String _value;

  MyType.hello(): _value = 'HELLO';
  MyType.world(): _value = 'WORLD';
  MyType.exclamationPoint(): _value = '!';

  @override
  String toString() {
    return _value;
  }
}

// Usage:
void main() {
  final hello = MyType.hello();
  final world = MyType.world();
  final punctuation = MyType.exclamationPoint();

  // Prints "HELLO, WORLD!"
  print("$hello, $world$punctuation");
}
0
Rick Gladwin On

Another way to enforce custom types in Dart is to use assertions in a constructor (when your custom type is being used).

class SomeClass {
  final String someVariable
  SomeClass(this.someVariable) : <initializer goes here>;
}

The initializer (the part after the colon at the end of the constructor) executes before the constructor itself, so custom requirements for your constructor variables can go there.

We use an assertion rather than an exception, since assertions throw errors that we want to catch in our code before production, rather than exceptions that we want to handle at runtime.

Let's say we have a class called Student that has a required field called id. The data type of id is String, but we want to enforce a rule that says a student id must be a 24 character hexadecimal string.

Like you say, the natural choice in TypeScript would be to create a custom type using a string literal or template literal and use that as the argument type when using a student id in a function and when instantiating a Student. Dart doesn't let us create custom data types in the same way (though there is the typedef type alias), but it does let us make assertions about the values of data types when we're trying to use them.

void main() {
  
  const validId = 'a52f5a6d7b46bffe9f5ec08f';
  const invalidId = 'thisisnotavalidid';
  
  // student1 throws no errors at development time, compile time, or runtime
  final student1 = Student(id: validId, fullName: 'Jimmy Beans');
  
  // student2 throws no errors at development time, but throws an
  // AssertionError (in debug mode) at runtime when trying to 
  // instantiate Student() with an invalid id.
  final student2 = Student(id: invalidId, fullName: 'Bimmy Jeans');
  
  print(student1.toString());
}

// 24 hexadecimal characters
const String studentIdPattern = r'^[a-f\d]{24}$';

class Student {
  
  final String id;
  final String fullName;
    
  Student({
    required this.id,
    required this.fullName,
  }) : assert(RegExp(studentIdPattern, caseSensitive: false).hasMatch(id));
  
  @override
  toString() {
    return "Instance of 'Student': {id: $id, fullName: $fullName}";
  }
}

Run this code on DartPad

Basically, we declare a regular expression pattern r'^[a-f\d]{24}$' for our 24 character hex string (it can be in the student.dart library/file or in some config file) and add an initializer to the default Student() constructor in the form of an assert() function call.

When the Student() constructor is called, the initializer runs first and checks that the id argument that has been passed in is valid.

Some additional notes regarding your specific example:

  • the regex pattern to use would be r'^(HELLO)|(WORLD)$'
  • that assert() call can be made in other places in the code where it matters what the value of the String type is; it doesn't have to be in an initializer.
  • this method does not throw errors in a linter by default (this is intentional on the part of the Dart devs). Make sure you run your code in a debugger, emulator, or test suite in order to trigger the assertions.
  • for more complicated custom types, there's a way to use Map<KeyClass, ValueClass> as a "custom type" and enums or classes for KeyClass and ValueClass in a way that's comparable to TypeScript types, but it's overkill for simple patterns.
2
Andrea Rossi On

Yes, today you can use typedefs from 2.13 dart version

typedef IntList = List<int>;
IntList il = [1, 2, 3];

https://dart.dev/language/typedefs