THX-1138
THX-1138

Reputation: 21730

Define TypeScript type that accepts object but not string

Short version:

I have an external type ExternalFoo that I can not modify. I need to build type Foo which is equivalent to the ExternalFoo except that it should not accept string.

type ExternalFoo = {} | number | string;
type Foo = ???;
const foo1: Foo = {}; // ok
const foo1: Foo = 5; // ok
const foo2: Foo = "bar"; // not ok 

Is it possible to define Foo based on the given ExternalFoo to satisfy this condition?

Longer version I want to build a parametarized type that can accept any given T and return T1 that is identical to T but doesn't allow for string.

I have a React app, and I want to force translation on it, we have a method forceTranslation method that accepts any React element, and changes specified properties such that they no longer accept string but can accept TranslatedString.

So if you have a component <User firstName={"bob"}/> the wrapped version (const TranslatedUser = forceTranslation(User, ["bob"]);) will compile-time fail if you give it "bob" but will succeed if you give it translate("bob").

The issue is that children property of type React.ReactNode include {} in its definition. Using Exclude<T, {}> will also exclude number (among other things) from the type.

Upvotes: 1

Views: 2085

Answers (2)

jcalz
jcalz

Reputation: 327704

The so-called "empty object type" {} is very wide; almost all values will be assignable to it. Roughly, it will accept any value that can be indexed into like an object. This includes all non-primitive types like object, but it also includes the five primitive types with wrapper objects: so string, number, boolean, bigint, and symbol are assignable to {} because values of these types are automatically wrapped in String, Number, Boolean, BigInt, and Symbol objects (respectively) when you access members on them as if they were objects.

In some sense this means your ExternalFoo type that cannot be changed is redundant: {} | string | number is basically equivalent to {} (but is both displayed differently and might be treated differently by the compiler in some circumstances). I'm wondering if the actual intent of ExternalFoo was supposed to be more like object | string | number instead of {} | string | number. Was it really meant to accept boolean, for example?

const wasThisIntended: ExternalFoo = true;

But you said you can't change it, so that's out of scope.


TypeScript does not have negated types (see microsoft/TypeScript#29317) so you cannot take ExternalFoo and quickly produce the version of it that rejects string. Such a type like ExternalFoo & not string is simply not representable in TypeScript as it currently exists. The Exclude utility type only filters unions. And while {} | number | string can be filtered to produce either {} | number, {} itself cannot be filtered because it is not a union.

In order to exclude string from ExternalFoo, we would need not only to remove string from the definition, but change {} to something that also excludes string. The type {} is more or less the same as object | string | number | boolean | bigint | symbol. We can, if we want, remove string from that, to produce

type Foo = object | number | boolean | bigint | symbol;

So let's try that!

const foo1: Foo = {}; // okay
const foo2: Foo = 5; // okay
const foo3: Foo = "bar"; // error!

Looks good. As desired, you can assign {} and 5 to a variable of type Foo, but you cannot assign "bar" to it. This still happens:

const stillHappens: Foo = true; // okay

But boolean is not a string so I guess it's desired also.


For your longer version, I'd suggest trying to convert a generic type T extends ExternalFoo to T & Foo and see if it works for you:

type HasLength = { length: number };
let l: HasLength;
l = [1, 2, 3]; // okay 
l = "hello"; // okay
l = { length: 19 }; // okay

type NotString<T extends ExternalFoo> = T & Foo;

type HasLengthNotString = NotString<{ length: number }>;
let n: HasLengthNotString;
n = [1, 2, 3]; // okay
n = "hello"; // error
n = { length: 19 }; // okay
  

Playground link to code

Upvotes: 2

Ardeshir Izadi
Ardeshir Izadi

Reputation: 1073

You can use Exclude utility.

type ExternalFoo = number | string;
type Foo = Exclude<ExternalFoo, string>;

const a: Foo = "hi"; // Error: Type 'string' is not assignable to type 'number'.ts(2322)

All built-in utilities in TypeScript: https://www.typescriptlang.org/docs/handbook/utility-types.html

Upvotes: -2

Related Questions