Matthew Layton
Matthew Layton

Reputation: 42229

TypeScript - Type constraints to allow only types, or literals and instances

Due to TypeScript's aggressive type erasure, whereby type annotations, interfaces, types and generic type parameters are all erased by the compiler, it's difficult to model certain type constraints. Below are examples of what I'm trying to model, and what I've managed to achieve so far.

A parameter MUST be the type itself, rather than an instance or literal of the type

This can be solved by introducing Type<T> as follows...

type Type<T = any> = {
    new(...args: any[]): T;
    readonly prototype: T;
    readonly name: string;
}

We can now define a function which constrains the parameter to a type, rather than instances or literals of the type...

function fn(p: Type<String>) { ... }

fn("") // not okay - it's a String literal
fn(new String()) // not okay - it's a String instance
fn(String) // okay - it's the String type

The reason that Type is generic, is so that you can specify any type can be passed in...

function fn(p: Type) { ... }

fn(String) // okay
fn(Number) // okay

A parameter MUST be an instance or literal, but NOT the type itself

This is less trivial, and so far I haven't been able to work this out. Given the following function definition...

function fn(p: String) { ... }

fn("") // okay - it's a String literal
fn(new String()) // okay - it's a String instance
fn(String) // not okay - it's the String type

That works fine, but it breaks when you want to allow any literal or instance, but disallow any type...

function fn(p: Object) { ... }

fn("") // okay - it's a String literal
fn(123) // okay - it's a Number literal
fn(new String()) // okay - it's a String instance
fn(String) // okay, BUT should be disallowed because it's the String type.

I'm not sure if the second problem can even be solved, however I would like something that aligns with Type<T>, but the inverse of it; for example...

type InstanceOrLiteral<T = any> = {
    // what goes here?
}

function fn(p: InstanceOrLiteral<String>) { ... }

fn("") // okay - it's a String literal
fn(new String()) // okay - it's a String instance
fn(123) // not okay - it's a Number literal
fn(String) // not okay - it's the String type

Equally, I'd like it to remain generic to allow any instance or literal, BUT disallow any type...

function fn(p: InstanceOrLiteral) { ... }

fn("") // okay - it's a String literal
fn(new String()) // okay - it's a String instance
fn(123) // okay - it's a Number literal
fn(String) // not okay - it's the String type
fn(Object) // not okay - it's the Object type
fn(Number) // not okay - it's the Number type

Is the latter possible?

Upvotes: 1

Views: 185

Answers (2)

MaximilianMairinger
MaximilianMairinger

Reputation: 2424

Your first issue can be resolve, by using a conditional type.

type InstanceOrLiteral<T, WhatInstancesAreDisallowed = unknown> = 
    T extends { new(): WhatInstancesAreDisallowed } ? never : T

Essentually this checks if given generic T extends a constructor (which returns the configurable generic WhatInstancesAreDisallowed). If so, the allowed type is never or in other words nothing (as never is a union of no types). When the conditional type does not match (ergo it is not a constructor) it defaults to T as the allowed types.

function fn(p: InstanceOrLiteral<String>) {}

fn("") // okay - it's a String literal
fn(new String()) // okay - it's a String instance
fn(123) // not okay - it's a Number literal
fn(String) // not okay - it's the String type

Your second issue requires a bit of trick. You cannot simply make T optional in your conditional type and default to unknown (= union of all types => would always match the conditional type) or any (= disables typing => would result in a union of the two options of the conditional type never | T which resolves to just T => hence would also not meet your requirements). Instead you have to infer the given type of the function call, and pass it into the InstanceOrLiteral type. So that the conditional type can work with the actual given type.

function fn2<T>(p: T & InstanceOrLiteral<T>) { }

fn2("") // okay - it's a String literal
fn2(new String()) // okay - it's a String instance
fn2(123) // okay - it's a Number literal
fn2(String) // not okay - it's the String type
fn2(Object) // not okay - it's the Object type
fn2(Number) // not okay - it's the Number type

Playground

Upvotes: 2

J&#243;zef Podlecki
J&#243;zef Podlecki

Reputation: 11283

fn(String) // okay, BUT should be disallowed because it's the String type.

No, its StringConstructor type. That's at least what typescript playground intellisense tells.

Can this work out for you?

function fn<K extends String | Number >(p: K) {

}

Upvotes: 0

Related Questions