1

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.

2 Answers 2

2

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

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

2 Comments

I'm working with React. The ExternalFoo in this case is ReactNode which includes {} through ReactFragment
I have also attempted to handle {} exclusion explicitly, i.e. do something like: object extends ExternalFoo ? Exclude<ExternalFoo, {} |string> | object | TranslatedString but that didn't work either.
-2

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

5 Comments

But you modified ExternalFoo from {} | number | string to number | string.
Yes @jcalz. Using {} is a bad practice, so I omitted it. As stated by TypeScript itself: Don't use {} as a type. {} actually means "any non-nullish value". - If you want a type meaning "any object", you probably want Record<string, unknown> instead. - If you want a type meaning "any value", you probably want unknown instead. - If you want a type meaning "empty object", you probably want Record<string, never> instead. You are right @jcalz. Thanks.
I'm not sure if you understood the context of my comment: the question says "I have an external type ExternalFoo that I cannot modify", so modifying ExternalFoo, no matter how nice it would be to do so, seems to be out of scope for the question. I agree that {} is probably not what anyone wants.
As pointed out by @jcalz. This side-steps my problem. Downvoting to reflect that.
Yes @jcalz. I understood. I just tried to point out the usage of Exclude. Thanks

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.