Can you create types with dynamic literals?

Say I have this function:
const createType = <T extends string>(code: T) => {
const literal: string = code;
return type({
code: `'${literal}'`
});
};
const createType = <T extends string>(code: T) => {
const literal: string = code;
return type({
code: `'${literal}'`
});
};
With this I'm trying to create types with literals but I don't know those literals until runtime. If I run:
const TypeONE = createType("ONE");
const TypeONE = createType("ONE");
It creates type:
const TypeONE: Type<{
code: string;
}, {}>
const TypeONE: Type<{
code: string;
}, {}>
But instead what I'm actually wanting is:
const TypeONE = type({
code: "'ONE'"
});
const TypeONE = type({
code: "'ONE'"
});
Which produces the correct type:
const TypeONE: Type<{
code: "ONE";
}, {}>
const TypeONE: Type<{
code: "ONE";
}, {}>
22 Replies
ssalbdivad
ssalbdivad4mo ago
Try:
code: `'${literal}'` as const
code: `'${literal}'` as const
This is a good approach but also keep in mind I'll be shipping native generics soon to make this more intuitive
SynthLuvr
SynthLuvr4mo ago
as const still causes it to be string I'll try narrow and pipe
ssalbdivad
ssalbdivad4mo ago
Ahh oh well I wasn't sure. Let me take a look I know I did some exploration of which strategies like this TS liked Oh wait You reassigned to string That is why it doesn't work
SynthLuvr
SynthLuvr4mo ago
const createType = <T extends string>(code: T) =>
type({
code: type("string")
.narrow((s) => s == code)
.pipe((s) => s as T),
});

const TypeONE = createType("ONE");
const createType = <T extends string>(code: T) =>
type({
code: type("string")
.narrow((s) => s == code)
.pipe((s) => s as T),
});

const TypeONE = createType("ONE");
This produces:
const TypeONE: Type<{
code: (In: string.narrowed) => Out<"ONE">;
}, {}>
const TypeONE: Type<{
code: (In: string.narrowed) => Out<"ONE">;
}, {}>
ssalbdivad
ssalbdivad4mo ago
Realistically '${string}' could validate but doesn't currently That would be the easiest thing For now you can just do this to work around it:
const createType = <T extends string>(literal: T) =>
type({
code: `'${literal}'` as type.cast<T>
})
const createType = <T extends string>(literal: T) =>
type({
code: `'${literal}'` as type.cast<T>
})
Even without the cast it actually is inferred correctly it just doesn't like the validation so you could also ignore the error or something if you wanted When you reassign a variable with an explicit type like this:
const literal: string = code;
const literal: string = code;
Whatever previous narrowed type it had is erased, in this case the association with the generic parameter
SynthLuvr
SynthLuvr4mo ago
Ok yes that works
ssalbdivad
ssalbdivad4mo ago
I guess I can't guarantee '${string}' is valid because it could have single quotes Meh
SynthLuvr
SynthLuvr4mo ago
Seems to behave differently within scope:
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;
return items;
}

const TypeONE = createType("ONE");
type TypeONE = typeof TypeONE.infer;
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;
return items;
}

const TypeONE = createType("ONE");
type TypeONE = typeof TypeONE.infer;
This type is valid:
type TypeONE = {
code: "ONE";
}[]
type TypeONE = {
code: "ONE";
}[]
However, this doesn't work:
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;

type TypeONE = typeof items.infer;
}
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>,
},
Items: "Item[]>0",
}).export().Items;

type TypeONE = typeof items.infer;
}
It becomes:
type TypeONE = {
code: _distill<T, "out", "base">;
}[]
type TypeONE = {
code: _distill<T, "out", "base">;
}[]
ssalbdivad
ssalbdivad4mo ago
It still looks right to me, it doesn't make sense to have the type be something specific inside the function
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>
},
Items: "Item[]>0"
}).export().Items

type TypeONE = typeof items.infer
return items({})
}

const r = createType("foo")

// R is typed as

type R = ArkErrors | {
code: "foo";
}[]
const createType = <T extends string>(code: T) => {
const items = scope({
Item: {
code: `'${code}'` as type.cast<T>
},
Items: "Item[]>0"
}).export().Items

type TypeONE = typeof items.infer
return items({})
}

const r = createType("foo")

// R is typed as

type R = ArkErrors | {
code: "foo";
}[]
SynthLuvr
SynthLuvr4mo ago
hmm. It's working for me without the scope, but I'm getting type error when using scope:
Type '{ message: string; code: T; }[]' is not assignable to type '{ message: string; code: _distill<T, "out", "base">; }[]'.
Type '{ message: string; code: T; }[]' is not assignable to type '{ message: string; code: _distill<T, "out", "base">; }[]'.
ssalbdivad
ssalbdivad4mo ago
You'll have to send a full repro I don't see a point in creating that intermediate type in the inner function
SynthLuvr
SynthLuvr4mo ago
const example = <T extends string>(code: T) => {
const Item = type({
code: `'${code}'` as type.cast<T>,
});
type Item = typeof Item.infer;

const createItem = (): Item => {
return { code };
};

const Items = scope({
Item,
Items: "Item[]>0",
}).export().Items;
type Items = typeof Items.infer;

const createItems = (): Items => {
return [{ code }];
};
};
const example = <T extends string>(code: T) => {
const Item = type({
code: `'${code}'` as type.cast<T>,
});
type Item = typeof Item.infer;

const createItem = (): Item => {
return { code };
};

const Items = scope({
Item,
Items: "Item[]>0",
}).export().Items;
type Items = typeof Items.infer;

const createItems = (): Items => {
return [{ code }];
};
};
ssalbdivad
ssalbdivad4mo ago
I don't know why TS simplifies it when using Type but not Scope, but it doesn't seem like something that could be changed externally. I'd also say if you're working with generics and nested functions this complex it's probably best to not worry about casting too much as even with no dependencies involved TS has a lot of limitations in that regard Maybe the example is more complex so you need the nested scope in the function, but if not you could also consider just using chaining and composition to build your types E.g. in a case like this you could use:
const Items = Item.array().atLeastLength(1)
const Items = Item.array().atLeastLength(1)
SynthLuvr
SynthLuvr4mo ago
I don't need the explicit return type there, I was only adding it to make sure my function was returning the correct type. If I infer it after creation then it's as I expect:
type Items = ReturnType<typeof createItems>;
type Items = ReturnType<typeof createItems>;
For some reason it doesn't think the type types match when they do
ssalbdivad
ssalbdivad4mo ago
Yeah best not to worry about it haha Like worry the external result is correct But trying to get correct inference in those intermediate steps will not always be possible
Want results from more Discord servers?
Add your server