I'm trying to create a type that defines the value based on the key. If the key extends $${string} (e.g. $foo) the value should be the key without the prefix e.g. foo. If the key doens't extend $${string} (e.g. boo) the values should be null.
Example
const example = {
$foo: 'foo',
boo: null,
}
Here is an isolated example I created to get it done - but it doesn't work as intended when I apply it to the code below. 😕
type Value<T> = T extends `$${infer I}` ? I : null
type ExampleA = Value<'$foo'> // type is 'foo'
type ExampleB = Value<'boo'> // type is null
My current code
type Values = {
[K in string]: K extends `$${infer p}` ? p : null;
}
const values = {
$foo: 'foo', // Type 'string' is not assignable to type 'null'.
foo: null,
$boo: 'boo', // Type 'string' is not assignable to type 'null'.
boo: null,
} satisfies Values;
type Expected = {
readonly $foo: 'foo',
readonly foo: null,
readonly $boo: 'boo',
readonly boo: null,
}
The satisfies Values is used to infer the type later on. Similar approach is acceptable🙂
Thanks for your help and time - cheers
The problem with your Values type is that mapped types over string do not behave the way you expect them to. While conceptually you can think of string as the infinite union of all possible string literal types, a mapped type over string does not even try to iterate over every possible string literal type; it just maps one thing: string:
type Values = {
[K in string]: K extends `\$${infer S}` ? S : null;
}
/* type Values = {
[x: string]: null;
} */
And since string does not extend `\$${infer S}`, then the property type for the string key is null.
This is working as intended, as discussed in microsoft/TypeScript#22509. Mapped types over string are not what you want.
And unfortunately there is no way to write a specific type in TypeScript which behaves the way you want. The closest you could get is something like
type Values = {
[k: `\$${string}`]: string;
[k: string]: string | null;
}
using a template string pattern index signature, but the parts where the property value string needs to match the part after the "$" character (not just string) and the part where other keys need to have a null (not just string | null) cannot be represented:
const values = {
$foo: 'foo',
foo: null,
$boo: 'boo',
boo: null,
$oops: null, // error, not string
oops: 'hmm', // should be error, but isn't!
$whoops: 'oops', // should be error, but isn't!
} satisfies Values;
So we have to give up on the approach using the satisfies operator, because there is no appropriate Values type to use it with.
What you really care about is having the type of values inferred by the compiler but still checked against your desired constraint. We can get behavior like this by replacing satisfies Values with a generic helper function we can call satisfiesValues(). At runtime this function just returns its input, but the compiler can use it to validate the object literal passed in. So instead of const values = {...} satisfies Values; you would write const values = satisfiesValues({...});.
Here's one possible implementation:
const satisfiesValues = <K extends PropertyKey>(
val: { [P in K]: P extends `\$${infer S}` ? S : null }
) => val;
The function is generic in K, the keys of the val value passed in. This will most likely be some union of known keys (none of which will be just string), and then the mapped type behaves as desired:
const values = satisfiesValues({
$foo: 'foo',
foo: null,
$boo: 'boo',
boo: null,
$oops: null, // error, not "oops"
oops: 'hmm', // error, not null
$whoops: 'oops', // error, not "whoops"
});
/* const values: {
foo: null;
$foo: "foo";
boo: null;
$boo: "boo";
oops: null;
$whoops: "whoops";
$oops: "oops";
} */
Looks good. The type of values is what you want it to be, and the compiler allows the valid properties and complains about the invalid ones.
Playground link to code
Related
Consider the following typescript enum:
enum MyEnum { A, B, C };
If I want another type that is the unioned strings of the keys of that enum, I can do the following:
type MyEnumKeysAsStrings = keyof typeof MyEnum; // "A" | "B" | "C"
This is very useful.
Now I want to create a generic type that operates universally on enums in this way, so that I can instead say:
type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<MyEnum>;
I imagine the correct syntax for that would be:
type AnyEnumKeysAsStrings<TEnum> = keyof typeof TEnum; // TS Error: 'TEnum' only refers to a type, but is being used as a value here.
But that generates a compile error: "'TEnum' only refers to a type, but is being used as a value here."
This is unexpected and sad. I can incompletely work around it the following way by dropping the typeof from the right side of the declaration of the generic, and adding it to the type parameter in the declaration of the specific type:
type AnyEnumAsUntypedKeys<TEnum> = keyof TEnum;
type MyEnumKeysAsStrings = AnyEnumAsUntypedKeys<typeof MyEnum>; // works, but not kind to consumer. Ick.
I don't like this workaround though, because it means the consumer has to remember to do this icky specifying of typeof on the generic.
Is there any syntax that will allow me to specify the generic type as I initially want, to be kind to the consumer?
No, the consumer will need to use typeof MyEnum to refer to the object whose keys are A, B, and C.
LONG EXPLANATION AHEAD, SOME OF WHICH YOU PROBABLY ALREADY KNOW
As you are likely aware, TypeScript adds a static type system to JavaScript, and that type system gets erased when the code is transpiled. The syntax of TypeScript is such that some expressions and statements refer to values that exist at runtime, while other expressions and statements refer to types that exist only at design/compile time. Values have types, but they are not types themselves. Importantly, there are some places in the code where the compiler will expect a value and interpret the expression it finds as a value if possible, and other places where the compiler will expect a type and interpret the expression it finds as a type if possible.
The compiler does not care or get confused if it is possible for an expression to be interpreted as both a value and a type. It is perfectly happy, for instance, with the two flavors of null in the following code:
let maybeString: string | null = null;
The first instance of null is a type and the second is a value. It also has no problem with
let Foo = {a: 0};
type Foo = {b: string};
where the first Foo is a named value and the second Foo is a named type. Note that the type of the value Foo is {a: number}, while the type Foo is {b: string}. They are not the same.
Even the typeof operator leads a double life. The expression typeof x always expects x to be a value, but typeof x itself could be a value or type depending on the context:
let bar = {a: 0};
let TypeofBar = typeof bar; // the value "object"
type TypeofBar = typeof bar; // the type {a: number}
The line let TypeofBar = typeof bar; will make it through to the JavaScript, and it will use the JavaScript typeof operator at runtime and produce a string. But type TypeofBar = typeof bar; is erased, and it is using the TypeScript type query operator to examine the static type that TypeScript has assigned to the value named bar.
Now, most language constructs in TypeScript that introduce names create either a named value or a named type. Here are some introductions of named values:
const value1 = 1;
let value2 = 2;
var value3 = 3;
function value4() {}
And here are some introductions of named types:
interface Type1 {}
type Type2 = string;
But there are a few declarations which create both a named value and a named type, and, like Foo above, the type of the named value is not the named type. The big ones are class and enum:
class Class { public prop = 0; }
enum Enum { A, B }
Here, the type Class is the type of an instance of Class, while the value Class is the constructor object. And typeof Class is not Class:
const instance = new Class(); // value instance has type (Class)
// type (Class) is essentially the same as {prop: number};
const ctor = Class; // value ctor has type (typeof Class)
// type (typeof Class) is essentially the same as new() => Class;
And, the type Enum is the type of an element of the enumeration; a union of the types of each element. While the value Enum is an object whose keys are A and B, and whose properties are the elements of the enumeration. And typeof Enum is not Enum:
const element = Math.random() < 0.5 ? Enum.A : Enum.B;
// value element has type (Enum)
// type (Enum) is essentially the same as Enum.A | Enum.B
// which is a subtype of (0 | 1)
const enumObject = Enum;
// value enumObject has type (typeof Enum)
// type (typeof Enum) is essentially the same as {A: Enum.A; B: Enum.B}
// which is a subtype of {A:0, B:1}
Backing way way up to your question now. You want to invent a type operator that works like this:
type KeysOfEnum = EnumKeysAsStrings<Enum>; // "A" | "B"
where you put the type Enum in, and get the keys of the object Enum out. But as you see above, the type Enum is not the same as the object Enum. And unfortunately the type doesn't know anything about the value. It is sort of like saying this:
type KeysOfEnum = EnumKeysAsString<0 | 1>; // "A" | "B"
Clearly if you write it like that, you'd see that there's nothing you could do to the type 0 | 1 which would produce the type "A" | "B". To make it work, you'd need to pass it a type that knows about the mapping. And that type is typeof Enum...
type KeysOfEnum = EnumKeysAsStrings<typeof Enum>;
which is like
type KeysOfEnum = EnumKeysAsString<{A:0, B:1}>; // "A" | "B"
which is possible... if type EnumKeysAsString<T> = keyof T.
So you are stuck making the consumer specify typeof Enum. Are there workarounds? Well, you could maybe use something that does that a value, such as a function?
function enumKeysAsString<TEnum>(theEnum: TEnum): keyof TEnum {
// eliminate numeric keys
const keys = Object.keys(theEnum).filter(x =>
(+x)+"" !== x) as (keyof TEnum)[];
// return some random key
return keys[Math.floor(Math.random()*keys.length)];
}
Then you can call
const someKey = enumKeysAsString(Enum);
and the type of someKey will be "A" | "B". Yeah but then to use it as type you'd have to query it:
type KeysOfEnum = typeof someKey;
which forces you to use typeof again and is even more verbose than your solution, especially since you can't do this:
type KeysOfEnum = typeof enumKeysAsString(Enum); // error
Blegh. Sorry.
To recap:
This is not possible
Types and values blah blah
Still not possible
Sorry
It actually is possible.
enum MyEnum { A, B, C };
type ObjectWithValuesOfEnumAsKeys = { [key in MyEnum]: string };
const a: ObjectWithValuesOfEnumAsKeys = {
"0": "Hello",
"1": "world",
"2": "!",
};
const b: ObjectWithValuesOfEnumAsKeys = {
[MyEnum.A]: "Hello",
[MyEnum.B]: "world",
[MyEnum.C]: "!",
};
// Property '2' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithValuesOfEnumAsKeys = { // Invalid! - Error here!
[MyEnum.A]: "Hello",
[MyEnum.B]: "world",
};
// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithValuesOfEnumAsKeys = {
[MyEnum.A]: "Hello",
[MyEnum.B]: "world",
[MyEnum.C]: "!",
6: "!", // Invalid! - Error here!
};
Playground Link
EDIT: Lifted limitation!
enum MyEnum { A, B, C };
type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = { [key in enumValues]: string };
const a: ObjectWithKeysOfEnumAsKeys = {
A: "Hello",
B: "world",
C: "!",
};
// Property 'C' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithKeysOfEnumAsKeys = { // Invalid! - Error here!
A: "Hello",
B: "world",
};
// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithKeysOfEnumAsKeys = {
A: "Hello",
B: "world",
C: "!",
D: "!", // Invalid! - Error here!
};
Playground Link
This work with const enum too!
There is a solution that doesn't require to create new generic types.
If you declare an enum
enum Season { Spring, Summer, Autumn, Winter };
To get to the type you only need to use the keywords keyof typeof
let seasonKey: keyof typeof Season;
Then the variable works as expected
seasonKey = "Autumn"; // is fine
// seasonKey = "AA" <= won't compile
You can just pass a type instead of a value and the compiler won't complain. This you achieve with typeof as you pointed out.
Will be just a bit less automatic:
type AnyEnumKeysAsStrings<TEnumType> = keyof TEnumType;
Which you can use as:
type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<typeof MyEnum>;
If I understand the OP question correctly and Akxe answer: Here is a possible further simplification. Use the typescript type utility. Record<Keys, Type>
https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type
e.g.
enum MyEnum { A, B, C };
type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = Record<enumValues, string>
const a: ObjectWithKeysOfEnumAsKeys = {
A: "PropertyA",
B: "PropertyB",
C: "PropertyC",
};
The problem itself is more complex than this but I'll try to explain in a simple way.
I have an object (it could be a different object, lets say a car o user or whatever).
const car = {
model: 'Model A',
specs: {
motor: {
turbo: true
}
}
};
Then I have a function to get a value from a property name.
interface Data<T, K extends keyof T> {
keynames: string[];
values: Array<T[K] | null>;
}
const getValues = <T, K extends keyof T>(keynames: string[], data: T): Data<T, K> => {
const values: Array<T[K] | null> = [];
keynames.forEach(keyname => {
const value: T[K] | undefined = getObjectValue(data, keyname);
if (typeof value === 'undefined') {
values.push(null);
} else {
values.push(value);
}
});
return {
keynames,
values
}
};
// This works with key.name syntax, specs.motor.turbo returns true for example
const getObjectValue = <T, K extends keyof T>(
object: T,
keyName: string
): T[K] | undefined => {
const keys = keyName.split('.');
if (keys.length === 1) {
if (typeof object === 'object') {
if (keys[0] in (object as T)) {
return (object as T)[keys[0] as K];
}
return undefined;
}
return undefined;
} else {
const [parentKey, ...restElements] = keys;
if (!object) return undefined;
return (getObjectValue(
(object as T)[parentKey as K],
restElements.join('.')
) as unknown) as T[K];
}
};
The problem becomes when I write some tests for example:
// ...
const data = getValues(['model', 'specs.motor.turbo'], car);
assert.deepEqual(data, {
keynames: ['model', 'specs.motor.turbo'],
values: ['Model A', true],
});
// ...
data is what I'm expecting, my function returns the same object as expected but I'm getting an error:
Type 'boolean' is not assignable to type 'string | { motor: { turbo: boolean; }; }'.ts(2322)
which is obvious since I'm only inferring T[K] in my function and not the type of the nested properties.
How to achieve that, so my function works with return types string | { motor: { turbo: boolean; }; } | { turbo: boolean; } | boolean. Or maybe there is a simpler way to return a nested property value.
I'm going to preserve your implementation as much as possible, even though there might be some improvements there that someone would suggest. I'm primarily taking this question as "how can I tell the TypeScript compiler what getValues() is doing?".
First, we need to represent what comes out when you deeply index into a type T with a dotted keypath K. This is only going to be possible in TypeScript 4.1 and later, since the following implementation relies both on template literal types as implemented in microsoft/TypeScript#40336, and recursive conditional types as implemented in microsoft/TypeScript#40002:
type DeepIndex<T, K extends string> = T extends object ? (
string extends K ? never :
K extends keyof T ? T[K] :
K extends `${infer F}.${infer R}` ? (F extends keyof T ?
DeepIndex<T[F], R> : never
) : never
) : never
The general plan here is to check if K is a key of T; if so, we do the lookup with T[K]. Otherwise we split K at the first dot into F and R, and recurse downward, indexing into T[F] with the key R. If at any point something goes wrong (F is not a valid key of T, for example), this returns never instead of property type. We will use this; if something becomes never then we assume that there was a bad index:
type ValidatePath<T, K> =
K extends string ? DeepIndex<T, K> extends never ? never : K : never;
The type ValidatePath<T, K> will extract from the (possible union of keys) K just those members which are valid paths.
Before we get into it, let's also represent what you do when you replace undefined with null:
type UndefinedToNull<T> = T extends undefined ? null : T;
For your code, I'm going to go ahead and use tuple types to represent the keynames and values arrays in Data. This should fall back to unordered arrays if necessary, but it seems weird to throw away information you know; for example, on your car example, you know that the first element of the values array is a string and the second is a boolean. A type like [string, boolean] has more information than Array<string | boolean | null>:
interface Data<T, K extends string[]> {
keynames: K;
values: { [I in keyof K]: UndefinedToNull<DeepIndex<T, Extract<K[I], string>>> };
}
The values property uses a mapped array/tuple to convert the tuple of keynames in K to a tuple of deep indexed values.
Now we need to give strong types to your function signatures. The compiler won't be able to verify a lot of the type safety inside of the implementations, so I'll be doing a lot of type assertions with as to force the compiler to accept what we're doing:
const getValues = <T, K extends string[]>(keynames: (K & { [I in keyof K]: ValidatePath<T, K[I]> }) | [], data: T): Data<T, K> => {
const values = [] as { [I in keyof K]: UndefinedToNull<DeepIndex<T, Extract<K[I], string>>> };
const _keynames = keynames as K;
_keynames.forEach(<I extends number>(keyname: K[I]) => {
const value: DeepIndex<T, Extract<K[I], string>> | undefined = getObjectValue(data, keyname as any);
if (typeof value === 'undefined') {
values.push(null as UndefinedToNull<DeepIndex<T, K[I]>>);
} else {
values.push(value as UndefinedToNull<DeepIndex<T, K[I]>>);
}
});
return {
keynames: _keynames,
values
}
};
const getObjectValue = <T, K extends string>(
object: T,
keyName: K & ValidatePath<T, K>
): DeepIndex<T, K> | undefined => {
const keys = keyName.split('.');
if (keys.length === 1) {
if (typeof object === 'object') {
if (keys[0] in (object as T)) {
return (object as T)[keys[0] as keyof T] as DeepIndex<T, K>;
}
return undefined;
}
return undefined;
} else {
const [parentKey, ...restElements] = keys;
if (!object) return undefined;
return (getObjectValue(
(object as T)[parentKey as keyof T],
restElements.join('.') as any as never
) as unknown) as DeepIndex<T, K>;
}
};
I wouldn't worry too much about the assertions inside the implementation. The important piece here is the call signature to getValues():
<T, K extends string[]>(keynames: (K & { [I in keyof K]: ValidatePath<T, K[I]> }) | [], data: T) => Data<T, K>
We are interpreting the keynames as something assignable to an array of string values. The | [] at the end is just a hint that the compiler should prefer viewing, say, ['model', 'specs.motor.turbo'] as an ordered pair instead of an unordered array. The intersection with {[I in keyof K]: ValidatePath<T, K[I]>}, a mapped tuple, should be a no-op is all the elements of K are valid paths into T. If any element of K is an invalid path, though, that element of the mapped tuple will be never and the validation will fail.
Let's test it:
const car = {
model: 'Model A',
specs: {
motor: {
turbo: true
}
}
}
const data = getValues(['model', 'specs.motor.turbo'], car);
data.keynames; // ["model", "specs.motor.turbo"]
data.values; // [string, boolean]
console.log(data);
This looks like exactly what you want with your example. What happens if we misspell a key?
getValues(['model', 'specs.motar.turbo'], car); // error!
// ---------------> ~~~~~~~~~~~~~~~~~~~
// Type 'string' is not assignable to type 'undefined' 🤷♂️
We get an error on the offending key. The error message isn't that useful, unfortunately. When I tried to use the answer to this question to give the compiler a full list of exactly which dotted paths to accept, it caused a massive slowdown and even some "type instantiation is excessively deep or infinite" errors. So while it would be nice to see "specs.motar.turbo" is not assignable to "model" | "specs" | "specs.motor" | "specs.motor.turbo"`, sadly I can't get that to happen in a reasonable way.
What if there could be an undefined value at the property in question?
const d2 = getValues(['a.b'], { a: Math.random() < 0.5 ? {} : { b: "hello" } });
d2.keynames; // ["a.b"]
d2.values; // [string | null]
console.log(d2);
It comes out as | null instead of | undefined, which is good.
So that works as well as I could get it to. There are undoubtedly limitations and edge cases. Deep indexing with dotted keys is kind of near the edge of what works in TypeScript (before 4.1 is was significantly past the edge, so that's something, right?). For example, I'd expect weird/bad things to happen with optional or union-typed properties at non-leaf nodes of the object tree. Or objects with string index signatures, for that matter. These might be addressable, but it would take some effort and a lot of testing. The point is: tread carefully.
Playground link to code
What I want to do
I'm currently looping over an object's keys and transferring the values to another object.
interface From {
[key: string]: string;
}
let from: From = {
prop1: "foo",
prop2: "23",
};
interface To {
[key: string]: string | number | boolean | undefined;
prop1: string;
prop2: number;
prop3: boolean;
}
let to: To = {} as To;
const initEnv = async () => {
const keys: string[] = Object.keys(from);
for (let i = 0; i < keys.length; i++) {
let key: string = keys[i];
let keyType = typeof (key as keyof To); // This only returns "string". Which kind of makes sense to me
let keyType = typeof keyof key; // SyntaxError: ',' expected
let keyType: typeof to[key]; // Error: 'key' refers to a value, but is being used as a type
to[key] = from[key];
}
};
I would want to be able to, say switch the value, so I don't want to just extract the type of the key. I want to assign it to a variable for use in the code, thus at runtime, as a string for instance.
So I think things like this wouldn't work.
let keyType2: typeof env[key]; // Error: 'key' refers to a value, but is being used as a type
Maybe, but the question is then; what do I assign to this variable?
The reason for all this, is that I want to convert the from variables to the correct type before, assigning them to the to object.
So yeah, basically, my question is how I would extract the type (as a string, at runtime, dynamically) from the key. Or is it even possible in the first place? And if it isn't why not? I like understanding things.
Thanks for putting up with my bad english, as well.
There are no interfaces at runtime; TypeScript's type system is erased from the emitted JavaScript. Your from and to values will be evaluated like this at runtime:
let from = {
prop1: "foo",
prop2: "23",
};
let to = {};
There's no From or To, and no way to use To to figure out how to coerce from's properties into the right types. The type system has no runtime effects.
The usefulness of TypeScript's type system comes from describing what will happen at runtime and not from affecting things at runtime. Imagine how you would have to write your code in pure JavaScript, and then give types to that code. Here's one way I might do it. Instead of a To interface, let's make a To object whose properties are functions that coerce inputs to other types:
const To = {
prop1: String,
prop2: Number,
prop3: Boolean
}
This is enough information to proceed at runtime.
Now, if you were going to build to manually, the compiler would be able to understand that the resulting value has a prop1 property of type string and a prop2 property of type number:
const toManual = { prop1: To.prop1(from.prop1), prop2: To.prop2(from.prop2) };
/* const toManual: { prop1: string; prop2: number; } */
But you don't want to do it manually; you'd like to write a loop that walks through the keys of from and uses To to produce properties of to. This is harder for the compiler to understand, but with judicious use of type assertions and type annotations you can write an objMap function that works programmatically:
function objMap<T, F extends { [K in keyof T]: (arg: T[K]) => any }>(
obj: T, fMap: F) {
const ret = {} as { [K in keyof T]: ReturnType<F[K]> };
const fM: { [K in keyof T]: (arg: T[K]) => ReturnType<F[K]> } = fMap;
(Object.keys(obj) as Array<keyof T>).forEach(<K extends keyof T>(k: K) => {
ret[k] = fM[k](obj[k]);
})
return ret;
}
The objMap function takes an object obj and a mapping object fMap which has at least all the same keys as obj and whose properties are functions that map obj's properties. The return type of objMap is an object whose properties are all the returned values of the fMap function for each property in obj. The actual work is being done by ret[k] = fM[k](obj[k]);. It's the programmatic equivalent of ret.prop1 = To.prop1(from.prop1); and ret.prop2 = To.prop2(from.prop2).
Let's see if it works:
const to = objMap(from, To);
/* const to: { prop1: string; prop2: number; } */
console.log(JSON.stringify(to));
// {"prop1":"foo","prop2":23}
That looks correct; the type of to is inferred by the compiler to be {prop1: string, prop2: number}, and the actual value of to is computed at runtime to be {prop1: "foo", prop2: 23}.
Okay, hope that helps; good luck!
Playground link to code
Problem
Is there a way in Typescript to define a type that is only a string literal, excluding string itself?
Note that I am not talking about a certain list of string literal; for which, a simple union of "Value1" | "Value2", or an enum type would work. I am talking about any string literal, but not string itself.
Example Code
type OnlyStringLiterals = ...; // <--- what should we put here?
const v1: OnlyStringLiterals = "hi"; // should work
const v2: OnlyStringLiterals = "bye"; // should work
// and so should be for any single string value assigned
// But:
const v3: OnlyStringLiterals = ("red" as string); // should NOT work -- it's string
Use Case
I am doing Branding on the types in my code, and I am passing a brand name, as a template, to my parent class. See the code below:
abstract class MyAbstractClass<
BRAND_T extends string,
VALUE_T = string
> {
constructor(private readonly _value: VALUE_T) { }
getValue(): VALUE_T { return this._value; }
private _Brand?: BRAND_T; // required to error on the last line, as intended!
}
class FirstName extends MyAbstractClass<"FirstName"> {
}
class AdminRole extends MyAbstractClass<"AdminRole"> {
}
class SubClassWithMissedName extends MyAbstractClass<string> {
// I want this to error! ........................ ^^^^^^
}
function printName(name: FirstName) {
console.log(name.getValue());
}
const userFirstName = new FirstName("Alex");
const userRole = new AdminRole("Moderator");
printName(userRole); // Already errors, as expected
Playground Link
I want to make sure every subclass is passing exactly a string literal, and not just string to the parent class.
I found an answer that works for my use case, but is not the most reusable one. Just sharing it anyway.
Thought Process
I believe it's not possible to have one solid type to represent what I wanted, because I cannot even think what will show up in VS Code if I hover over it!
However, to my knowledge, there is a function-style checking in Typescript for types that you can pass a type in and expect a type back, and finally assign a value to it to see if it goes through.
Type-checking using a Generic Type and a follow-up assignment
Using this technique I am thinking about the following template type:
type TrueStringLiterals<T extends string> = string extends T ? never : true;
const v1 = "hi";
const check1: TrueStringLiterals<typeof v1> = true; // No error :-)
const v2 = "bye";
const check2: TrueStringLiterals<typeof v2> = true; // No error :-)
const v3 = ("red" as string);
const check3: TrueStringLiterals<typeof v3> = true; // Errors, as expected!
Playground Link
Easier in an already-passed Generic Type
Also, in my use case, I am doing:
abstract class MyAbstractClass<
BRAND_T extends (string extends BRAND_T ? never : string),
VALUE_T = string
> {
...
Playground Link
... which works like a charm!
You can create utility type which will allow only on subset of string:
type SubString<T> = T extends string ?
string extends T ? never
: T
: never
const makeSubStr = <T extends string>(a: SubString<T>) => a
const a = makeSubStr('strLiteral')
const b = makeSubStr('strLiteral' as string) // error
const c: string = 'elo I am string'
const d = makeSubStr(c) // error
const e: SubString<"red"> = ("red" as string); // error
This type will also return never if something is not a string, in your answer TrueStringLiterals will not take this case into consideration and pass it through.
The other answers don't catch the case where the provided type parameter is a union of literal strings. If this shall be explicitly avoided, as could be read from the OPs question, the following solution, based on the other two can be used:
type UnUnion<T, S> = T extends S ? ([S] extends [T] ? T : never) : never;
type NotUnion<T> = UnUnion<T, T>;
type LiteralString<T extends string> = string extends T ? never : NotUnion<T>;
where UnUnion uses the fact that if T is a union, say 'a' | 'b', the union is distributed over the rest of the type expression.
(['a'|'b'] extends ['a'] ? ... ) | (['a'|'b'] extends ['b'] ? ...)
If T is a union, none of these can hold and all the parts turn into never.
NotUnion reduces this to have just one generic parameter and LiteralString just uses its result in case its parameter is not extendable by string.
Playground Link
I'd like to submit an answer from a similar question I recently asked, that is far more simple than the examples given so far:
type SpecificString<S extends Exclude<string, S>> = S
let test1: SpecificString<"a" | "b" | "c"> // okay
let test2: SpecificString<string> // error
//guaranteed to work where `Exclude<string, T>` wouldn't
let test3: Exclude<SpecificString<"a" | "1">, "1">
test3 = "a" // okay
test3 = "1" // error
Basically how this works:
Exclude<string, "any string literal"> ==> resolves to string
Exclude<string, string> ==> resolves to never
You can call this F-bounded quantification if you like I guess.
In TypeScript, how should one go about having the compiler determine whether or not a string is a valid argument to a method/function?
Right now, I am using string literals to accomplish this. For example,
type ValidLetter = "A" | "B" | "C" | "D"; // string literal definition
public PostLetter(letter: ValidLetter) {
...
api.post("https://example.com/letters/", letter);
// POST method only accepts "A", "B", "C", or "D"
}
PostLetter("A") // All good!
PostLetter("Z") // Compiler error
The only thing is, at compile time, I don't know the values I will be passing in to the Post method. I could be receiving any kind of string,
let a = "A";
let foobar = "foobar";
PostLetter(a) // Compiler error
PostLetter(foobar) // Compiler error
What I'm looking for is a way of checking if a string is a valid member of a string literal. I've already attempted using typeof, instanceof, user-defined type guards, and casting. None of them seem to have what it takes.
How would I go about determining that a is a member of ValidLetter and foobar is not? Or perhaps string literals are not the way to go.
TypeScript simply does not do any runtime type checking. Type checking happens at compile time, and type information isn't included in the produced JavaScript file.
post.ts
type ValidLetter = "A" | "B" | "C";
function post(letter: ValidLetter) {
}
Produces the following JavaScript:
post.js
function post(letter) {
}
So you have to re-specify the type check yourself manually in runtime code:
type ValidLetter = "A" | "B" | "C";
function post(letter: ValidLetter) {
if (letter !== "A" && letter !== "B" && letter !== "C") throw "error!";
}
Not too bad. But it's a bit redundant, isn't it?
There's a library called runtypes, that allows you to specify your types once, and it produces a compile-time TypeScript type, as well as keeping type information to do runtime checks:
import { Literal, Union, Static } from 'runtypes'
const ValidLetter = Union(Literal('A'), Literal('B'), Literal('C'));
type ValidLetter = Static<typeof ValidLetter>;
function post(letter: ValidLetter) {
ValidLetter.check(letter);
}
So now you get both full compile-time checks, and runtime checks.
You should be able to do this with a mix of value maps, as well as the user-defined type guards:
const ValidLetterMap = { A: 1, B: 1, C: 1, D: 1 };
type ValidLetter = keyof typeof ValidLetterMap;
declare function postLetter(letter: ValidLetter): void;
postLetter("A"); // ok
postLetter("E"); // err
const a = "A";
postLetter(a); // ok
let a$ = "A";
postLetter(a$); // err, a$ is of type string since it is mutable
function isValidLetter(letter: string): letter is ValidLetter {
return letter in ValidLetterMap;
}
if (isValidLetter(a$)) {
postLetter(a$); // now ok because we've "proven" that a$ is a valid letter
}
Edit: here's a generic form, relying on a slight hack to expose typing.
class StringLiteral<T extends string> {
private literalSet: {[P in T]: true};
// sort of a hack so we can expose a union type of valid letters
public get typeProvider(): T {
throw new Error("typeProvider is only meant for typing info, it has no value");
}
constructor(...literals: T[]) {
this.literalSet = literals.reduce(
(acc, curr) => (acc[curr] = true, acc),
{} as {[P in T]: true}
);
}
public isValid(candidate: string): candidate is T {
return candidate in this.literalSet;
}
}
// how to use
const lettersLiteral = new StringLiteral("A", "B", "C", "D");
declare function postLetter(letter: typeof lettersLiteral.typeProvider): void;
let a$ = "A";
postLetter(a$); // not ok
if (lettersLiteral.isValid(a$)) {
postLetter(a$); // ok!!
}